mito-ai 0.1.36__py3-none-any.whl → 0.1.37__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mito-ai might be problematic. Click here for more details.

Files changed (54) hide show
  1. mito_ai/__init__.py +6 -4
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +3 -10
  4. mito_ai/app_builder/handlers.py +89 -11
  5. mito_ai/app_builder/models.py +3 -0
  6. mito_ai/auth/README.md +18 -0
  7. mito_ai/auth/__init__.py +6 -0
  8. mito_ai/auth/handlers.py +96 -0
  9. mito_ai/auth/urls.py +13 -0
  10. mito_ai/completions/completion_handlers/chat_completion_handler.py +2 -2
  11. mito_ai/completions/models.py +7 -6
  12. mito_ai/completions/prompt_builders/agent_execution_prompt.py +8 -3
  13. mito_ai/completions/prompt_builders/agent_system_message.py +21 -7
  14. mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
  15. mito_ai/completions/prompt_builders/utils.py +53 -10
  16. mito_ai/constants.py +11 -1
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +112 -0
  18. mito_ai/streamlit_conversion/streamlit_system_prompt.py +42 -0
  19. mito_ai/streamlit_conversion/streamlit_utils.py +96 -0
  20. mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +207 -0
  21. mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
  22. mito_ai/tests/streamlit_conversion/__init__.py +3 -0
  23. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +265 -0
  24. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +197 -0
  25. mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +418 -0
  26. mito_ai/tests/test_constants.py +18 -3
  27. mito_ai/utils/anthropic_utils.py +18 -70
  28. mito_ai/utils/gemini_utils.py +22 -73
  29. mito_ai/utils/mito_server_utils.py +147 -4
  30. mito_ai/utils/open_ai_utils.py +18 -107
  31. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  32. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  33. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  34. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js → mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js +1165 -539
  35. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js.map +1 -0
  36. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5c9333902dce30642119.js → mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js +18 -14
  37. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js.map +1 -0
  38. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js → mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +6 -2
  39. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
  40. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/METADATA +1 -1
  41. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/RECORD +51 -38
  42. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.a20772bc113422d0f505.js.map +0 -1
  43. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5c9333902dce30642119.js.map +0 -1
  44. mito_ai-0.1.36.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.76efcc5c3be4056457ee.js.map +0 -1
  45. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  46. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  47. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  48. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
  49. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
  50. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  51. {mito_ai-0.1.36.data → mito_ai-0.1.37.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  52. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/WHEEL +0 -0
  53. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/entry_points.txt +0 -0
  54. {mito_ai-0.1.36.dist-info → mito_ai-0.1.37.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,3 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
@@ -0,0 +1,265 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ from unittest.mock import patch, AsyncMock
6
+ from mito_ai.streamlit_conversion.streamlit_agent_handler import (
7
+ StreamlitCodeGeneration,
8
+ streamlit_handler
9
+ )
10
+ from typing import cast
11
+
12
+ # Add this line to enable async support
13
+ pytest_plugins = ('pytest_asyncio',)
14
+
15
+
16
+ class TestStreamlitCodeGeneration:
17
+ """Test cases for StreamlitCodeGeneration class"""
18
+
19
+ def test_init(self):
20
+ """Test StreamlitCodeGeneration initialization"""
21
+ notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
22
+ generator = StreamlitCodeGeneration(notebook_data)
23
+
24
+ assert len(generator.messages) == 1
25
+ assert generator.messages[0]["role"] == "user"
26
+ # Access content properly as a list and cast to expected type
27
+ content_list = cast(list, generator.messages[0]["content"])
28
+ assert isinstance(content_list, list)
29
+ assert len(content_list) > 0
30
+ content_item = cast(dict, content_list[0])
31
+ assert content_item["type"] == "text"
32
+ assert "jupyter notebook content" in content_item["text"]
33
+
34
+ @pytest.mark.asyncio
35
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
36
+ @pytest.mark.parametrize("mock_items,expected_result", [
37
+ (["Hello", " World", "!"], "Hello World!"),
38
+ ([], ""),
39
+ (["Here's your code: import streamlit"], "Here's your code: import streamlit")
40
+ ])
41
+ async def test_get_response_from_agent(self, mock_stream, mock_items, expected_result):
42
+ """Test response from agent with different scenarios"""
43
+ # Mock the async generator
44
+ async def mock_async_gen():
45
+ for item in mock_items:
46
+ yield item
47
+
48
+ mock_stream.return_value = mock_async_gen()
49
+
50
+ notebook_data: dict = {"cells": []}
51
+ generator = StreamlitCodeGeneration(notebook_data)
52
+
53
+ result = await generator.get_response_from_agent(generator.messages)
54
+
55
+ assert result == expected_result
56
+ mock_stream.assert_called_once()
57
+
58
+
59
+ @pytest.mark.asyncio
60
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
61
+ async def test_get_response_from_agent_exception(self, mock_stream):
62
+ """Test exception handling in get_response_from_agent"""
63
+ mock_stream.side_effect = Exception("API Error")
64
+
65
+ notebook_data: dict = {"cells": []}
66
+ generator = StreamlitCodeGeneration(notebook_data)
67
+
68
+ with pytest.raises(Exception, match="API Error"):
69
+ await generator.get_response_from_agent(generator.messages)
70
+
71
+ def test_add_agent_response_to_context(self):
72
+ """Test adding agent response to message history"""
73
+ notebook_data: dict = {"cells": []}
74
+ generator = StreamlitCodeGeneration(notebook_data)
75
+
76
+ initial_count = len(generator.messages)
77
+ generator.add_agent_response_to_context("Test response")
78
+
79
+ assert len(generator.messages) == initial_count + 1
80
+ assert generator.messages[-1]["role"] == "assistant"
81
+ # Access content properly as a list and cast to expected type
82
+ content_list = cast(list, generator.messages[-1]["content"])
83
+ assert isinstance(content_list, list)
84
+ assert len(content_list) > 0
85
+ content_item = cast(dict, content_list[0])
86
+ assert content_item["type"] == "text"
87
+ assert content_item["text"] == "Test response"
88
+
89
+ @pytest.mark.asyncio
90
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
91
+ async def test_generate_streamlit_code_success(self, mock_stream):
92
+ """Test successful streamlit code generation"""
93
+ mock_response = "Here's your code:\n```python\nimport streamlit\nst.title('Hello')\n```"
94
+
95
+ async def mock_async_gen():
96
+ for item in [mock_response]:
97
+ yield item
98
+
99
+ mock_stream.return_value = mock_async_gen()
100
+
101
+ notebook_data: dict = {"cells": []}
102
+ generator = StreamlitCodeGeneration(notebook_data)
103
+
104
+ result = await generator.generate_streamlit_code()
105
+
106
+ expected_code = "import streamlit\nst.title('Hello')\n"
107
+ assert result == expected_code
108
+
109
+ # Check that response was added to context
110
+ assert len(generator.messages) == 2
111
+ assert generator.messages[-1]["role"] == "assistant"
112
+
113
+ @pytest.mark.asyncio
114
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
115
+ async def test_correct_error_in_generation_success(self, mock_stream):
116
+ """Test successful error correction"""
117
+ mock_response = "Here's the corrected code:\n```python\nimport streamlit\nst.title('Fixed')\n```"
118
+ async def mock_async_gen():
119
+ for item in [mock_response]:
120
+ yield item
121
+
122
+ mock_stream.return_value = mock_async_gen()
123
+
124
+ notebook_data: dict = {"cells": []}
125
+ generator = StreamlitCodeGeneration(notebook_data)
126
+
127
+ result = await generator.correct_error_in_generation("ImportError: No module named 'pandas'")
128
+
129
+ expected_code = "import streamlit\nst.title('Fixed')\n"
130
+ assert result == expected_code
131
+
132
+ # Check that error message and response were added to context
133
+ assert len(generator.messages) == 3
134
+ assert generator.messages[-2]["role"] == "user"
135
+ assert generator.messages[-1]["role"] == "assistant"
136
+
137
+ @pytest.mark.asyncio
138
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
139
+ async def test_correct_error_in_generation_exception(self, mock_stream):
140
+ """Test exception handling in error correction"""
141
+ mock_stream.side_effect = Exception("API Error")
142
+
143
+ notebook_data: dict = {"cells": []}
144
+ generator = StreamlitCodeGeneration(notebook_data)
145
+
146
+ with pytest.raises(Exception, match="API Error"):
147
+ await generator.correct_error_in_generation("Some error")
148
+
149
+
150
+ class TestStreamlitHandler:
151
+ """Test cases for streamlit_handler function"""
152
+
153
+ @pytest.mark.asyncio
154
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
155
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
156
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.streamlit_code_validator')
157
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
158
+ async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generator_class, mock_parse):
159
+ """Test successful streamlit handler execution"""
160
+ # Mock notebook parsing
161
+ mock_notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
162
+ mock_parse.return_value = mock_notebook_data
163
+
164
+ # Mock code generation
165
+ mock_generator = AsyncMock()
166
+ mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
167
+ mock_generator_class.return_value = mock_generator
168
+
169
+ # Mock validation (no errors)
170
+ mock_validator.return_value = (False, "")
171
+
172
+ # Mock file creation
173
+ mock_create_file.return_value = (True, "File created successfully")
174
+
175
+ result = await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
176
+
177
+ assert result[0] is True
178
+ assert "File created successfully" in result[1]
179
+
180
+ # Verify calls
181
+ mock_parse.assert_called_once_with("/path/to/notebook.ipynb")
182
+ mock_generator_class.assert_called_once_with(mock_notebook_data)
183
+ mock_generator.generate_streamlit_code.assert_called_once()
184
+ mock_validator.assert_called_once_with("import streamlit\nst.title('Test')")
185
+ mock_create_file.assert_called_once_with("/path/to/app", "import streamlit\nst.title('Test')")
186
+
187
+ @pytest.mark.asyncio
188
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
189
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
190
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.streamlit_code_validator')
191
+ async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_generator_class, mock_parse):
192
+ """Test streamlit handler when max retries are exceeded"""
193
+ # Mock notebook parsing
194
+ mock_notebook_data: dict = {"cells": []}
195
+ mock_parse.return_value = mock_notebook_data
196
+
197
+ # Mock code generation
198
+ mock_generator = AsyncMock()
199
+ mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
200
+ mock_generator.correct_error_in_generation.return_value = "import streamlit\nst.title('Fixed')"
201
+ mock_generator_class.return_value = mock_generator
202
+
203
+ # Mock validation (always errors)
204
+ mock_validator.return_value = (True, "Persistent error")
205
+
206
+ result = await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
207
+
208
+ assert result[0] is False
209
+ assert "Error generating streamlit code by agent" in result[1]
210
+
211
+ # Verify that error correction was called 5 times (max retries)
212
+ assert mock_generator.correct_error_in_generation.call_count == 5
213
+
214
+ @pytest.mark.asyncio
215
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
216
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
217
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.streamlit_code_validator')
218
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
219
+ async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generator_class, mock_parse):
220
+ """Test streamlit handler when file creation fails"""
221
+ # Mock notebook parsing
222
+ mock_notebook_data: dict = {"cells": []}
223
+ mock_parse.return_value = mock_notebook_data
224
+
225
+ # Mock code generation
226
+ mock_generator = AsyncMock()
227
+ mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
228
+ mock_generator_class.return_value = mock_generator
229
+
230
+ # Mock validation (no errors)
231
+ mock_validator.return_value = (False, "")
232
+
233
+ # Mock file creation failure
234
+ mock_create_file.return_value = (False, "Permission denied")
235
+
236
+ result = await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
237
+
238
+ assert result[0] is False
239
+ assert "Permission denied" in result[1]
240
+
241
+ @pytest.mark.asyncio
242
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
243
+ async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
244
+ """Test streamlit handler when notebook parsing fails"""
245
+ mock_parse.side_effect = FileNotFoundError("Notebook not found")
246
+
247
+ with pytest.raises(FileNotFoundError, match="Notebook not found"):
248
+ await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
249
+
250
+ @pytest.mark.asyncio
251
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
252
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
253
+ async def test_streamlit_handler_generation_exception(self, mock_generator_class, mock_parse):
254
+ """Test streamlit handler when code generation fails"""
255
+ # Mock notebook parsing
256
+ mock_notebook_data: dict = {"cells": []}
257
+ mock_parse.return_value = mock_notebook_data
258
+
259
+ # Mock code generation failure
260
+ mock_generator = AsyncMock()
261
+ mock_generator.generate_streamlit_code.side_effect = Exception("Generation failed")
262
+ mock_generator_class.return_value = mock_generator
263
+
264
+ with pytest.raises(Exception, match="Generation failed"):
265
+ await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
@@ -0,0 +1,197 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
+
4
+ import pytest
5
+ import json
6
+ import tempfile
7
+ import os
8
+ from unittest.mock import patch, mock_open
9
+ from mito_ai.streamlit_conversion.streamlit_utils import (
10
+ extract_code_blocks,
11
+ create_app_file,
12
+ parse_jupyter_notebook_to_extract_required_content
13
+ )
14
+ from typing import Dict, Any
15
+
16
+
17
+ class TestExtractCodeBlocks:
18
+ """Test cases for extract_code_blocks function"""
19
+
20
+ def test_extract_code_blocks_with_python_blocks(self):
21
+ """Test extracting code from message with python code blocks"""
22
+ message = "Here's some code:\n```python\nimport streamlit\nst.title('Hello')\n```\nThat's it!"
23
+ result = extract_code_blocks(message)
24
+ expected = "import streamlit\nst.title('Hello')\n"
25
+ assert result == expected
26
+
27
+ def test_extract_code_blocks_without_python_blocks(self):
28
+ """Test when message doesn't contain python code blocks"""
29
+ message = "This is just regular text without code blocks"
30
+ result = extract_code_blocks(message)
31
+ assert result == message
32
+
33
+ def test_extract_code_blocks_empty_message(self):
34
+ """Test with empty message"""
35
+ message = ""
36
+ result = extract_code_blocks(message)
37
+ assert result == message
38
+
39
+ def test_extract_code_blocks_multiple_blocks(self):
40
+ """Test extracting from first python block when multiple exist"""
41
+ message = "```python\ncode1\n```\n```python\ncode2\n```"
42
+ result = extract_code_blocks(message)
43
+ expected = "code1\n\ncode2\n"
44
+ assert result == expected
45
+
46
+
47
+ class TestCreateAppFile:
48
+ """Test cases for create_app_file function"""
49
+
50
+ def test_create_app_file_success(self, tmp_path):
51
+ """Test successful file creation"""
52
+ file_path = str(tmp_path)
53
+ code = "import streamlit\nst.title('Test')"
54
+
55
+ success, message = create_app_file(file_path, code)
56
+
57
+ assert success is True
58
+ assert "Successfully created" in message
59
+
60
+ # Verify file was created with correct content
61
+ app_file_path = os.path.join(file_path, "app.py")
62
+ assert os.path.exists(app_file_path)
63
+
64
+ with open(app_file_path, 'r') as f:
65
+ content = f.read()
66
+ assert content == code
67
+
68
+ def test_create_app_file_io_error(self):
69
+ """Test file creation with IO error"""
70
+ file_path = "/nonexistent/path/that/should/fail"
71
+ code = "import streamlit"
72
+
73
+ success, message = create_app_file(file_path, code)
74
+
75
+ assert success is False
76
+ assert "Error creating file" in message
77
+
78
+ @patch('builtins.open', side_effect=Exception("Unexpected error"))
79
+ def test_create_app_file_unexpected_error(self, mock_open):
80
+ """Test file creation with unexpected error"""
81
+ file_path = "/tmp/test"
82
+ code = "import streamlit"
83
+
84
+ success, message = create_app_file(file_path, code)
85
+
86
+ assert success is False
87
+ assert "Unexpected error" in message
88
+
89
+ def test_create_app_file_empty_code(self, tmp_path):
90
+ """Test creating file with empty code"""
91
+ file_path = str(tmp_path)
92
+ code = ""
93
+
94
+ success, message = create_app_file(file_path, code)
95
+
96
+ assert success is True
97
+ assert "Successfully created" in message
98
+
99
+ app_file_path = os.path.join(file_path, "app.py")
100
+ with open(app_file_path, 'r') as f:
101
+ content = f.read()
102
+ assert content == ""
103
+
104
+
105
+ class TestParseJupyterNotebookToExtractRequiredContent:
106
+ """Test cases for parse_jupyter_notebook_to_extract_required_content function"""
107
+
108
+ def test_parse_valid_notebook(self, tmp_path):
109
+ """Test parsing a valid notebook with cells"""
110
+ notebook_data: Dict[str, Any] = {
111
+ "cells": [
112
+ {
113
+ "cell_type": "code",
114
+ "source": ["import pandas as pd\n", "df = pd.DataFrame()\n"],
115
+ "metadata": {"some": "metadata"},
116
+ "execution_count": 1
117
+ },
118
+ {
119
+ "cell_type": "markdown",
120
+ "source": ["# Title\n", "Some text\n"],
121
+ "metadata": {"another": "metadata"}
122
+ }
123
+ ],
124
+ "metadata": {"notebook_metadata": "value"},
125
+ "nbformat": 4
126
+ }
127
+
128
+ notebook_path = tmp_path / "test.ipynb"
129
+ with open(notebook_path, 'w') as f:
130
+ json.dump(notebook_data, f)
131
+
132
+ result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
133
+
134
+ # Check that only cell_type and source are preserved
135
+ assert len(result['cells']) == 2
136
+ assert result['cells'][0]['cell_type'] == 'code'
137
+ assert result['cells'][0]['source'] == ["import pandas as pd\n", "df = pd.DataFrame()\n"]
138
+ assert 'metadata' not in result['cells'][0]
139
+ assert 'execution_count' not in result['cells'][0]
140
+
141
+ assert result['cells'][1]['cell_type'] == 'markdown'
142
+ assert result['cells'][1]['source'] == ["# Title\n", "Some text\n"]
143
+ assert 'metadata' not in result['cells'][1]
144
+
145
+ def test_parse_notebook_file_not_found(self):
146
+ """Test parsing non-existent notebook file"""
147
+ with pytest.raises(FileNotFoundError, match="Notebook file not found"):
148
+ parse_jupyter_notebook_to_extract_required_content("/nonexistent/path/notebook.ipynb")
149
+
150
+ def test_parse_notebook_with_missing_cell_fields(self, tmp_path):
151
+ """Test parsing notebook where cells are missing cell_type or source"""
152
+ notebook_data: Dict[str, Any] = {
153
+ "cells": [
154
+ {
155
+ "cell_type": "code"
156
+ # Missing source field
157
+ },
158
+ {
159
+ "source": ["some text"]
160
+ # Missing cell_type field
161
+ },
162
+ {
163
+ "cell_type": "markdown",
164
+ "source": ["# Title"]
165
+ }
166
+ ]
167
+ }
168
+
169
+ notebook_path = tmp_path / "test.ipynb"
170
+ with open(notebook_path, 'w') as f:
171
+ json.dump(notebook_data, f)
172
+
173
+ result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
174
+
175
+ assert len(result['cells']) == 3
176
+ assert result['cells'][0]['cell_type'] == 'code'
177
+ assert result['cells'][0]['source'] == [] # Default empty list
178
+
179
+ assert result['cells'][1]['cell_type'] == '' # Default empty string
180
+ assert result['cells'][1]['source'] == ["some text"]
181
+
182
+ assert result['cells'][2]['cell_type'] == 'markdown'
183
+ assert result['cells'][2]['source'] == ["# Title"]
184
+
185
+ def test_parse_empty_notebook(self, tmp_path):
186
+ """Test parsing notebook with empty cells list"""
187
+ notebook_data: Dict[str, Any] = {
188
+ "cells": []
189
+ }
190
+
191
+ notebook_path = tmp_path / "test.ipynb"
192
+ with open(notebook_path, 'w') as f:
193
+ json.dump(notebook_data, f)
194
+
195
+ result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
196
+
197
+ assert result['cells'] == []