mito-ai 0.1.37__py3-none-any.whl → 0.1.39__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.
- mito_ai/__init__.py +17 -1
- mito_ai/_version.py +1 -1
- mito_ai/app_builder/handlers.py +43 -38
- mito_ai/app_builder/models.py +1 -1
- mito_ai/completions/handlers.py +1 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +18 -45
- mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
- mito_ai/log/handlers.py +10 -3
- mito_ai/log/urls.py +3 -3
- mito_ai/openai_client.py +1 -1
- mito_ai/streamlit_conversion/agent_utils.py +116 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +59 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +44 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +90 -44
- mito_ai/streamlit_conversion/streamlit_system_prompt.py +30 -17
- mito_ai/streamlit_conversion/streamlit_utils.py +48 -8
- mito_ai/streamlit_conversion/validate_streamlit_app.py +116 -0
- mito_ai/streamlit_preview/__init__.py +7 -0
- mito_ai/streamlit_preview/handlers.py +164 -0
- mito_ai/streamlit_preview/manager.py +159 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +166 -78
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +4 -5
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +119 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +302 -0
- mito_ai/tests/utils/test_anthropic_utils.py +2 -2
- mito_ai/utils/anthropic_utils.py +4 -4
- mito_ai/utils/open_ai_utils.py +0 -4
- mito_ai/utils/telemetry_utils.py +28 -1
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +6 -1
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js +799 -116
- mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js.map +1 -0
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js +5 -5
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.93ecc9bc0edba61535cc.js.map → mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.606207904e6aaa42b1bf.js.map +1 -1
- {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/METADATA +4 -1
- {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/RECORD +53 -42
- mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +0 -207
- mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +0 -418
- mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js.map +0 -1
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
3
|
|
|
4
4
|
import pytest
|
|
5
|
-
from unittest.mock import patch, AsyncMock
|
|
5
|
+
from unittest.mock import patch, AsyncMock, MagicMock
|
|
6
6
|
from mito_ai.streamlit_conversion.streamlit_agent_handler import (
|
|
7
7
|
StreamlitCodeGeneration,
|
|
8
8
|
streamlit_handler
|
|
9
9
|
)
|
|
10
|
-
from
|
|
10
|
+
from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
|
|
11
11
|
|
|
12
12
|
# Add this line to enable async support
|
|
13
13
|
pytest_plugins = ('pytest_asyncio',)
|
|
@@ -16,20 +16,24 @@ pytest_plugins = ('pytest_asyncio',)
|
|
|
16
16
|
class TestStreamlitCodeGeneration:
|
|
17
17
|
"""Test cases for StreamlitCodeGeneration class"""
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
|
|
21
|
+
async def test_init(self, mock_stream):
|
|
20
22
|
"""Test StreamlitCodeGeneration initialization"""
|
|
23
|
+
# Mock the async generator
|
|
24
|
+
async def mock_async_gen():
|
|
25
|
+
yield "Here's your code:\n```python\nimport streamlit\nst.title('Test')\n```"
|
|
26
|
+
|
|
27
|
+
mock_stream.return_value = mock_async_gen()
|
|
28
|
+
|
|
21
29
|
notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
assert
|
|
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"]
|
|
30
|
+
streamlit_code_handler = StreamlitCodeGeneration()
|
|
31
|
+
|
|
32
|
+
streamlit_code = await streamlit_code_handler.generate_streamlit_code(notebook_data)
|
|
33
|
+
|
|
34
|
+
assert streamlit_code is not None
|
|
35
|
+
assert len(streamlit_code) > 0
|
|
36
|
+
assert "import streamlit" in streamlit_code
|
|
33
37
|
|
|
34
38
|
@pytest.mark.asyncio
|
|
35
39
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
|
|
@@ -48,9 +52,9 @@ class TestStreamlitCodeGeneration:
|
|
|
48
52
|
mock_stream.return_value = mock_async_gen()
|
|
49
53
|
|
|
50
54
|
notebook_data: dict = {"cells": []}
|
|
51
|
-
|
|
55
|
+
streamlit_code_handler = StreamlitCodeGeneration()
|
|
52
56
|
|
|
53
|
-
result = await
|
|
57
|
+
result = await streamlit_code_handler.generate_streamlit_code(notebook_data)
|
|
54
58
|
|
|
55
59
|
assert result == expected_result
|
|
56
60
|
mock_stream.assert_called_once()
|
|
@@ -63,28 +67,10 @@ class TestStreamlitCodeGeneration:
|
|
|
63
67
|
mock_stream.side_effect = Exception("API Error")
|
|
64
68
|
|
|
65
69
|
notebook_data: dict = {"cells": []}
|
|
66
|
-
|
|
70
|
+
streamlit_code_handler = StreamlitCodeGeneration()
|
|
67
71
|
|
|
68
72
|
with pytest.raises(Exception, match="API Error"):
|
|
69
|
-
await
|
|
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"
|
|
73
|
+
await streamlit_code_handler.generate_streamlit_code(notebook_data)
|
|
88
74
|
|
|
89
75
|
@pytest.mark.asyncio
|
|
90
76
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
|
|
@@ -99,22 +85,26 @@ class TestStreamlitCodeGeneration:
|
|
|
99
85
|
mock_stream.return_value = mock_async_gen()
|
|
100
86
|
|
|
101
87
|
notebook_data: dict = {"cells": []}
|
|
102
|
-
|
|
88
|
+
streamlit_code_handler = StreamlitCodeGeneration()
|
|
103
89
|
|
|
104
|
-
result = await
|
|
90
|
+
result = await streamlit_code_handler.generate_streamlit_code(notebook_data)
|
|
105
91
|
|
|
106
92
|
expected_code = "import streamlit\nst.title('Hello')\n"
|
|
107
93
|
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
94
|
|
|
113
95
|
@pytest.mark.asyncio
|
|
114
96
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
|
|
115
97
|
async def test_correct_error_in_generation_success(self, mock_stream):
|
|
116
98
|
"""Test successful error correction"""
|
|
117
|
-
mock_response = "
|
|
99
|
+
mock_response = """```unified_diff
|
|
100
|
+
--- a/app.py
|
|
101
|
+
+++ b/app.py
|
|
102
|
+
@@ -1,1 +1,1 @@
|
|
103
|
+
-import streamlit
|
|
104
|
+
-st.title('Test')
|
|
105
|
+
+import streamlit
|
|
106
|
+
+st.title('Fixed')
|
|
107
|
+
```"""
|
|
118
108
|
async def mock_async_gen():
|
|
119
109
|
for item in [mock_response]:
|
|
120
110
|
yield item
|
|
@@ -122,29 +112,23 @@ class TestStreamlitCodeGeneration:
|
|
|
122
112
|
mock_stream.return_value = mock_async_gen()
|
|
123
113
|
|
|
124
114
|
notebook_data: dict = {"cells": []}
|
|
125
|
-
|
|
115
|
+
streamlit_code_handler = StreamlitCodeGeneration()
|
|
126
116
|
|
|
127
|
-
result = await
|
|
117
|
+
result = await streamlit_code_handler.correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
|
|
128
118
|
|
|
129
119
|
expected_code = "import streamlit\nst.title('Fixed')\n"
|
|
130
120
|
assert result == expected_code
|
|
131
121
|
|
|
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
122
|
@pytest.mark.asyncio
|
|
138
123
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
|
|
139
124
|
async def test_correct_error_in_generation_exception(self, mock_stream):
|
|
140
125
|
"""Test exception handling in error correction"""
|
|
141
126
|
mock_stream.side_effect = Exception("API Error")
|
|
142
127
|
|
|
143
|
-
|
|
144
|
-
generator = StreamlitCodeGeneration(notebook_data)
|
|
128
|
+
streamlit_code_handler = StreamlitCodeGeneration()
|
|
145
129
|
|
|
146
130
|
with pytest.raises(Exception, match="API Error"):
|
|
147
|
-
await
|
|
131
|
+
await streamlit_code_handler.correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
|
|
148
132
|
|
|
149
133
|
|
|
150
134
|
class TestStreamlitHandler:
|
|
@@ -153,9 +137,10 @@ class TestStreamlitHandler:
|
|
|
153
137
|
@pytest.mark.asyncio
|
|
154
138
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
155
139
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
|
|
156
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
140
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
157
141
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
158
|
-
|
|
142
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
|
|
143
|
+
async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
|
|
159
144
|
"""Test successful streamlit handler execution"""
|
|
160
145
|
# Mock notebook parsing
|
|
161
146
|
mock_notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
|
|
@@ -170,43 +155,47 @@ class TestStreamlitHandler:
|
|
|
170
155
|
mock_validator.return_value = (False, "")
|
|
171
156
|
|
|
172
157
|
# Mock file creation
|
|
173
|
-
mock_create_file.return_value = (True, "File created successfully")
|
|
158
|
+
mock_create_file.return_value = (True, "/path/to/app.py", "File created successfully")
|
|
159
|
+
|
|
160
|
+
# Mock clean directory check (no-op)
|
|
161
|
+
mock_clean_directory.return_value = None
|
|
174
162
|
|
|
175
|
-
result = await streamlit_handler("/path/to/notebook.ipynb"
|
|
163
|
+
result = await streamlit_handler("/path/to/notebook.ipynb")
|
|
176
164
|
|
|
177
165
|
assert result[0] is True
|
|
178
|
-
assert "File created successfully" in result[
|
|
166
|
+
assert "File created successfully" in result[2]
|
|
179
167
|
|
|
180
168
|
# Verify calls
|
|
181
169
|
mock_parse.assert_called_once_with("/path/to/notebook.ipynb")
|
|
182
|
-
mock_generator_class.assert_called_once_with(
|
|
170
|
+
mock_generator_class.assert_called_once_with()
|
|
183
171
|
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
|
|
172
|
+
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", "/path/to/notebook.ipynb")
|
|
173
|
+
mock_create_file.assert_called_once_with("/path/to", "import streamlit\nst.title('Test')")
|
|
186
174
|
|
|
187
175
|
@pytest.mark.asyncio
|
|
188
176
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
189
177
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
|
|
190
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
178
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
191
179
|
async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_generator_class, mock_parse):
|
|
192
180
|
"""Test streamlit handler when max retries are exceeded"""
|
|
193
181
|
# Mock notebook parsing
|
|
194
182
|
mock_notebook_data: dict = {"cells": []}
|
|
195
183
|
mock_parse.return_value = mock_notebook_data
|
|
196
|
-
|
|
184
|
+
|
|
197
185
|
# Mock code generation
|
|
198
186
|
mock_generator = AsyncMock()
|
|
199
187
|
mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
|
|
200
188
|
mock_generator.correct_error_in_generation.return_value = "import streamlit\nst.title('Fixed')"
|
|
201
189
|
mock_generator_class.return_value = mock_generator
|
|
190
|
+
|
|
191
|
+
# Mock validation (always errors) - Return list of errors as expected by validate_app
|
|
192
|
+
mock_validator.return_value = (True, ["Persistent error"])
|
|
193
|
+
|
|
194
|
+
result = await streamlit_handler("/notebook.ipynb")
|
|
202
195
|
|
|
203
|
-
#
|
|
204
|
-
mock_validator.return_value = (True, "Persistent error")
|
|
205
|
-
|
|
206
|
-
result = await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
|
|
207
|
-
|
|
196
|
+
# Verify the result indicates failure
|
|
208
197
|
assert result[0] is False
|
|
209
|
-
assert "Error generating streamlit code by agent" in result[
|
|
198
|
+
assert "Error generating streamlit code by agent" in result[2]
|
|
210
199
|
|
|
211
200
|
# Verify that error correction was called 5 times (max retries)
|
|
212
201
|
assert mock_generator.correct_error_in_generation.call_count == 5
|
|
@@ -214,9 +203,10 @@ class TestStreamlitHandler:
|
|
|
214
203
|
@pytest.mark.asyncio
|
|
215
204
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
216
205
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
|
|
217
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
206
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
218
207
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
219
|
-
|
|
208
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
|
|
209
|
+
async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
|
|
220
210
|
"""Test streamlit handler when file creation fails"""
|
|
221
211
|
# Mock notebook parsing
|
|
222
212
|
mock_notebook_data: dict = {"cells": []}
|
|
@@ -231,26 +221,34 @@ class TestStreamlitHandler:
|
|
|
231
221
|
mock_validator.return_value = (False, "")
|
|
232
222
|
|
|
233
223
|
# Mock file creation failure
|
|
234
|
-
mock_create_file.return_value = (False, "Permission denied")
|
|
224
|
+
mock_create_file.return_value = (False, None, "Permission denied")
|
|
225
|
+
|
|
226
|
+
# Mock clean directory check (no-op)
|
|
227
|
+
mock_clean_directory.return_value = None
|
|
235
228
|
|
|
236
|
-
result = await streamlit_handler("/path/to/notebook.ipynb"
|
|
229
|
+
result = await streamlit_handler("/path/to/notebook.ipynb")
|
|
237
230
|
|
|
238
231
|
assert result[0] is False
|
|
239
|
-
assert "Permission denied" in result[
|
|
232
|
+
assert "Permission denied" in result[2]
|
|
240
233
|
|
|
241
234
|
@pytest.mark.asyncio
|
|
242
235
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
243
|
-
|
|
236
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
|
|
237
|
+
async def test_streamlit_handler_parse_notebook_exception(self, mock_clean_directory, mock_parse):
|
|
244
238
|
"""Test streamlit handler when notebook parsing fails"""
|
|
239
|
+
# Mock clean directory check (no-op)
|
|
240
|
+
mock_clean_directory.return_value = None
|
|
241
|
+
|
|
245
242
|
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
246
243
|
|
|
247
244
|
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
248
|
-
await streamlit_handler("/path/to/notebook.ipynb"
|
|
245
|
+
await streamlit_handler("/path/to/notebook.ipynb")
|
|
249
246
|
|
|
250
247
|
@pytest.mark.asyncio
|
|
251
248
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
252
249
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
|
|
253
|
-
|
|
250
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
|
|
251
|
+
async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generator_class, mock_parse):
|
|
254
252
|
"""Test streamlit handler when code generation fails"""
|
|
255
253
|
# Mock notebook parsing
|
|
256
254
|
mock_notebook_data: dict = {"cells": []}
|
|
@@ -261,5 +259,95 @@ class TestStreamlitHandler:
|
|
|
261
259
|
mock_generator.generate_streamlit_code.side_effect = Exception("Generation failed")
|
|
262
260
|
mock_generator_class.return_value = mock_generator
|
|
263
261
|
|
|
262
|
+
# Mock clean directory check (no-op)
|
|
263
|
+
mock_clean_directory.return_value = None
|
|
264
|
+
|
|
264
265
|
with pytest.raises(Exception, match="Generation failed"):
|
|
265
|
-
await streamlit_handler("/path/to/notebook.ipynb"
|
|
266
|
+
await streamlit_handler("/path/to/notebook.ipynb")
|
|
267
|
+
|
|
268
|
+
@pytest.mark.asyncio
|
|
269
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
270
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
|
|
271
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
272
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
273
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
|
|
274
|
+
async def test_streamlit_handler_too_many_files_in_directory(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
|
|
275
|
+
"""Test streamlit handler when there are too many files in the directory"""
|
|
276
|
+
# Mock clean directory check to raise ValueError (simulating >10 files)
|
|
277
|
+
mock_clean_directory.side_effect = ValueError("Too many files in directory: 10 allowed but 15 present. Create a new directory and retry")
|
|
278
|
+
|
|
279
|
+
# The function should raise the ValueError before any other processing
|
|
280
|
+
with pytest.raises(ValueError, match="Too many files in directory: 10 allowed but 15 present. Create a new directory and retry"):
|
|
281
|
+
await streamlit_handler("/path/to/notebook.ipynb")
|
|
282
|
+
|
|
283
|
+
# Verify that clean_directory_check was called
|
|
284
|
+
mock_clean_directory.assert_called_once_with("/path/to/notebook.ipynb")
|
|
285
|
+
|
|
286
|
+
# Verify that no other functions were called since the error occurred early
|
|
287
|
+
mock_parse.assert_not_called()
|
|
288
|
+
mock_generator_class.assert_not_called()
|
|
289
|
+
mock_validator.assert_not_called()
|
|
290
|
+
mock_create_file.assert_not_called()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TestCleanDirectoryCheck:
|
|
294
|
+
"""Test cases for clean_directory_check function"""
|
|
295
|
+
|
|
296
|
+
@patch('mito_ai.streamlit_conversion.streamlit_utils.Path')
|
|
297
|
+
def test_clean_directory_check_under_limit(self, mock_path):
|
|
298
|
+
"""Test clean_directory_check when directory has 10 or fewer files"""
|
|
299
|
+
# Mock the Path class and its methods
|
|
300
|
+
mock_path_instance = mock_path.return_value
|
|
301
|
+
mock_path_instance.resolve.return_value = mock_path_instance
|
|
302
|
+
mock_path_instance.parent = mock_path_instance
|
|
303
|
+
|
|
304
|
+
# Mock directory existence check
|
|
305
|
+
mock_path_instance.exists.return_value = True
|
|
306
|
+
|
|
307
|
+
# Mock directory contents with 8 files
|
|
308
|
+
mock_files = []
|
|
309
|
+
for i in range(8):
|
|
310
|
+
mock_file = MagicMock()
|
|
311
|
+
mock_file.is_file.return_value = True
|
|
312
|
+
mock_files.append(mock_file)
|
|
313
|
+
|
|
314
|
+
mock_path_instance.iterdir.return_value = mock_files
|
|
315
|
+
|
|
316
|
+
# Should not raise any exception
|
|
317
|
+
clean_directory_check('/path/to/notebook.ipynb')
|
|
318
|
+
|
|
319
|
+
# Verify calls
|
|
320
|
+
mock_path.assert_called_once_with('/path/to/notebook.ipynb')
|
|
321
|
+
mock_path_instance.resolve.assert_called_once()
|
|
322
|
+
mock_path_instance.exists.assert_called_once()
|
|
323
|
+
mock_path_instance.iterdir.assert_called_once()
|
|
324
|
+
|
|
325
|
+
@patch('mito_ai.streamlit_conversion.streamlit_utils.Path')
|
|
326
|
+
def test_clean_directory_check_over_limit(self, mock_path):
|
|
327
|
+
"""Test clean_directory_check when directory has more than 10 files"""
|
|
328
|
+
# Mock the Path class and its methods
|
|
329
|
+
mock_path_instance = mock_path.return_value
|
|
330
|
+
mock_path_instance.resolve.return_value = mock_path_instance
|
|
331
|
+
mock_path_instance.parent = mock_path_instance
|
|
332
|
+
|
|
333
|
+
# Mock directory existence check
|
|
334
|
+
mock_path_instance.exists.return_value = True
|
|
335
|
+
|
|
336
|
+
# Mock directory contents with 15 files
|
|
337
|
+
mock_files = []
|
|
338
|
+
for i in range(15):
|
|
339
|
+
mock_file = MagicMock()
|
|
340
|
+
mock_file.is_file.return_value = True
|
|
341
|
+
mock_files.append(mock_file)
|
|
342
|
+
|
|
343
|
+
mock_path_instance.iterdir.return_value = mock_files
|
|
344
|
+
|
|
345
|
+
# Should raise ValueError
|
|
346
|
+
with pytest.raises(ValueError, match="Too many files in directory: 10 allowed but 15 present. Create a new directory and retry"):
|
|
347
|
+
clean_directory_check('/path/to/notebook.ipynb')
|
|
348
|
+
|
|
349
|
+
# Verify calls
|
|
350
|
+
mock_path.assert_called_once_with('/path/to/notebook.ipynb')
|
|
351
|
+
mock_path_instance.resolve.assert_called_once()
|
|
352
|
+
mock_path_instance.exists.assert_called_once()
|
|
353
|
+
mock_path_instance.iterdir.assert_called_once()
|
|
@@ -13,7 +13,6 @@ from mito_ai.streamlit_conversion.streamlit_utils import (
|
|
|
13
13
|
)
|
|
14
14
|
from typing import Dict, Any
|
|
15
15
|
|
|
16
|
-
|
|
17
16
|
class TestExtractCodeBlocks:
|
|
18
17
|
"""Test cases for extract_code_blocks function"""
|
|
19
18
|
|
|
@@ -52,7 +51,7 @@ class TestCreateAppFile:
|
|
|
52
51
|
file_path = str(tmp_path)
|
|
53
52
|
code = "import streamlit\nst.title('Test')"
|
|
54
53
|
|
|
55
|
-
success, message = create_app_file(file_path, code)
|
|
54
|
+
success, app_path, message = create_app_file(file_path, code)
|
|
56
55
|
|
|
57
56
|
assert success is True
|
|
58
57
|
assert "Successfully created" in message
|
|
@@ -70,7 +69,7 @@ class TestCreateAppFile:
|
|
|
70
69
|
file_path = "/nonexistent/path/that/should/fail"
|
|
71
70
|
code = "import streamlit"
|
|
72
71
|
|
|
73
|
-
success, message = create_app_file(file_path, code)
|
|
72
|
+
success, app_path, message = create_app_file(file_path, code)
|
|
74
73
|
|
|
75
74
|
assert success is False
|
|
76
75
|
assert "Error creating file" in message
|
|
@@ -81,7 +80,7 @@ class TestCreateAppFile:
|
|
|
81
80
|
file_path = "/tmp/test"
|
|
82
81
|
code = "import streamlit"
|
|
83
82
|
|
|
84
|
-
success, message = create_app_file(file_path, code)
|
|
83
|
+
success, app_path, message = create_app_file(file_path, code)
|
|
85
84
|
|
|
86
85
|
assert success is False
|
|
87
86
|
assert "Unexpected error" in message
|
|
@@ -91,7 +90,7 @@ class TestCreateAppFile:
|
|
|
91
90
|
file_path = str(tmp_path)
|
|
92
91
|
code = ""
|
|
93
92
|
|
|
94
|
-
success, message = create_app_file(file_path, code)
|
|
93
|
+
success, app_path, message = create_app_file(file_path, code)
|
|
95
94
|
|
|
96
95
|
assert success is True
|
|
97
96
|
assert "Successfully created" in message
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from unittest.mock import patch, MagicMock
|
|
7
|
+
from mito_ai.streamlit_conversion.validate_streamlit_app import (
|
|
8
|
+
StreamlitValidator,
|
|
9
|
+
validate_app
|
|
10
|
+
)
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestStreamlitValidator:
|
|
15
|
+
"""Test cases for StreamlitValidator class"""
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize("code,expected_error,test_description", [
|
|
18
|
+
# Valid Python code should return no error
|
|
19
|
+
(
|
|
20
|
+
"import streamlit\nst.title('Hello World')",
|
|
21
|
+
None,
|
|
22
|
+
"valid Python code"
|
|
23
|
+
),
|
|
24
|
+
# Invalid Python syntax should be caught
|
|
25
|
+
(
|
|
26
|
+
"import streamlit\nst.title('Hello World'",
|
|
27
|
+
"SyntaxError",
|
|
28
|
+
"invalid Python code"
|
|
29
|
+
),
|
|
30
|
+
# Empty streamlit app is valid
|
|
31
|
+
(
|
|
32
|
+
"",
|
|
33
|
+
None,
|
|
34
|
+
"empty code"
|
|
35
|
+
),
|
|
36
|
+
])
|
|
37
|
+
def test_validate_syntax(self, code, expected_error, test_description):
|
|
38
|
+
"""Test syntax validation with various code inputs"""
|
|
39
|
+
validator = StreamlitValidator()
|
|
40
|
+
|
|
41
|
+
error = validator.get_syntax_error(code)
|
|
42
|
+
|
|
43
|
+
if expected_error is None:
|
|
44
|
+
assert error is None, f"Expected no error for {test_description}"
|
|
45
|
+
else:
|
|
46
|
+
assert error is not None, f"Expected error for {test_description}"
|
|
47
|
+
assert expected_error in error, f"Expected '{expected_error}' in error for {test_description}"
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("app_code,expected_error", [
|
|
50
|
+
("x = 5", None),
|
|
51
|
+
("1/0", "division by zero"),
|
|
52
|
+
("", None)
|
|
53
|
+
])
|
|
54
|
+
def test_get_runtime_errors(self, app_code, expected_error):
|
|
55
|
+
"""Test getting runtime errors"""
|
|
56
|
+
validator = StreamlitValidator()
|
|
57
|
+
|
|
58
|
+
errors = validator.get_runtime_errors(app_code, '/app.py')
|
|
59
|
+
|
|
60
|
+
if expected_error is None:
|
|
61
|
+
assert errors is None
|
|
62
|
+
else:
|
|
63
|
+
errors_str = str(errors)
|
|
64
|
+
assert expected_error in errors_str
|
|
65
|
+
|
|
66
|
+
def test_get_runtime_errors_with_relative_path(self):
|
|
67
|
+
"""Test getting runtime errors"""
|
|
68
|
+
|
|
69
|
+
app_code ="""
|
|
70
|
+
import streamlit as st
|
|
71
|
+
import pandas as pd
|
|
72
|
+
|
|
73
|
+
df=pd.read_csv('data.csv')
|
|
74
|
+
"""
|
|
75
|
+
# Create a temporary csv file in the directory temp/data.csv
|
|
76
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
77
|
+
directory = 'app_directory'
|
|
78
|
+
csv_path = os.path.join(temp_dir, directory, "data.csv")
|
|
79
|
+
|
|
80
|
+
os.makedirs(os.path.join(temp_dir, directory), exist_ok=True)
|
|
81
|
+
app_path = os.path.join(temp_dir, directory, "app.py")
|
|
82
|
+
|
|
83
|
+
# Create the file if it doesn't exist
|
|
84
|
+
with open(csv_path, "w") as f:
|
|
85
|
+
f.write("name,age\nJohn,25\nJane,30")
|
|
86
|
+
|
|
87
|
+
validator = StreamlitValidator()
|
|
88
|
+
errors = validator.get_runtime_errors(app_code, app_path)
|
|
89
|
+
assert errors is None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@patch('subprocess.Popen')
|
|
93
|
+
def test_cleanup_with_process(self, mock_popen):
|
|
94
|
+
"""Test cleanup with running process"""
|
|
95
|
+
validator = StreamlitValidator()
|
|
96
|
+
validator.temp_dir = "/tmp/test_dir"
|
|
97
|
+
|
|
98
|
+
# Mock directory exists
|
|
99
|
+
with patch('os.path.exists', return_value=True):
|
|
100
|
+
with patch('shutil.rmtree') as mock_rmtree:
|
|
101
|
+
validator.cleanup()
|
|
102
|
+
|
|
103
|
+
mock_rmtree.assert_called_once()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@pytest.mark.parametrize("app_code,expected_has_validation_error,expected_error_message", [
|
|
107
|
+
("x=5", False, ""),
|
|
108
|
+
("1/0", True, "division by zero"),
|
|
109
|
+
("print('Hello World'", True, "SyntaxError"),
|
|
110
|
+
("", False, ""),
|
|
111
|
+
])
|
|
112
|
+
def test_streamlit_code_validator(self, app_code, expected_has_validation_error, expected_error_message):
|
|
113
|
+
|
|
114
|
+
has_validation_error, errors = validate_app(app_code, '/app.py')
|
|
115
|
+
|
|
116
|
+
assert has_validation_error == expected_has_validation_error
|
|
117
|
+
assert expected_error_message in str(errors)
|
|
118
|
+
|
|
119
|
+
|