mito-ai 0.1.46__py3-none-any.whl → 0.1.49__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.
- mito_ai/_version.py +1 -1
- mito_ai/app_deploy/app_deploy_utils.py +28 -9
- mito_ai/app_deploy/handlers.py +123 -84
- mito_ai/app_deploy/models.py +19 -12
- mito_ai/completions/models.py +6 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +13 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
- mito_ai/completions/prompt_builders/utils.py +13 -0
- mito_ai/path_utils.py +70 -0
- mito_ai/streamlit_conversion/agent_utils.py +4 -201
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
- mito_ai/streamlit_conversion/streamlit_utils.py +13 -75
- mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
- mito_ai/streamlit_preview/__init__.py +1 -2
- mito_ai/streamlit_preview/handlers.py +54 -85
- mito_ai/streamlit_preview/manager.py +11 -18
- mito_ai/streamlit_preview/utils.py +12 -28
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +22 -4
- mito_ai/tests/message_history/test_message_history_utils.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +40 -60
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +87 -57
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +27 -40
- mito_ai/user/handlers.py +15 -3
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/message_history_utils.py +3 -1
- mito_ai/utils/telemetry_utils.py +78 -13
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +3571 -1442
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +24 -24
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/METADATA +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/RECORD +71 -69
- mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
- mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
- /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_apis_signOut_mjs-node_module-75790d.688c25857e7b81b1740f.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_react-dom_client_js-node_modules_aws-amplify_ui-react_dist_styles_css.b43d4249e4d3dac9ad7b.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.49.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.46.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,7 +12,7 @@ from mito_ai.streamlit_conversion.streamlit_agent_handler import (
|
|
|
12
12
|
correct_error_in_generation,
|
|
13
13
|
streamlit_handler
|
|
14
14
|
)
|
|
15
|
-
from mito_ai.
|
|
15
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
|
|
16
16
|
|
|
17
17
|
# Add this line to enable async support
|
|
18
18
|
pytest_plugins = ('pytest_asyncio',)
|
|
@@ -102,14 +102,12 @@ class TestCorrectErrorInGeneration:
|
|
|
102
102
|
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
103
103
|
async def test_correct_error_in_generation_success(self, mock_stream):
|
|
104
104
|
"""Test successful error correction"""
|
|
105
|
-
mock_response = """```
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
+import streamlit
|
|
112
|
-
+st.title('Fixed')
|
|
105
|
+
mock_response = """```search_replace
|
|
106
|
+
>>>>>>> SEARCH
|
|
107
|
+
st.title('Test')
|
|
108
|
+
=======
|
|
109
|
+
st.title('Fixed')
|
|
110
|
+
<<<<<<< REPLACE
|
|
113
111
|
```"""
|
|
114
112
|
async def mock_async_gen():
|
|
115
113
|
for item in [mock_response]:
|
|
@@ -117,8 +115,8 @@ class TestCorrectErrorInGeneration:
|
|
|
117
115
|
|
|
118
116
|
mock_stream.return_value = mock_async_gen()
|
|
119
117
|
|
|
120
|
-
result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
|
|
121
|
-
|
|
118
|
+
result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
|
|
119
|
+
|
|
122
120
|
expected_code = "import streamlit\nst.title('Fixed')\n"
|
|
123
121
|
assert result == expected_code
|
|
124
122
|
|
|
@@ -140,8 +138,8 @@ class TestStreamlitHandler:
|
|
|
140
138
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
141
139
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
142
140
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
143
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
144
|
-
async def test_streamlit_handler_success(self,
|
|
141
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_conversion_success')
|
|
142
|
+
async def test_streamlit_handler_success(self, mock_log_success, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
145
143
|
"""Test successful streamlit handler execution"""
|
|
146
144
|
# Mock notebook parsing
|
|
147
145
|
mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
|
|
@@ -151,35 +149,30 @@ class TestStreamlitHandler:
|
|
|
151
149
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
152
150
|
|
|
153
151
|
# Mock validation (no errors)
|
|
154
|
-
mock_validator.return_value =
|
|
155
|
-
|
|
156
|
-
# Mock file creation
|
|
157
|
-
mock_create_file.return_value = (True, "/path/to/app.py", "File created successfully")
|
|
158
|
-
|
|
159
|
-
# Mock clean directory check (no-op)
|
|
160
|
-
mock_clean_directory.return_value = None
|
|
152
|
+
mock_validator.return_value = []
|
|
161
153
|
|
|
162
154
|
# Use a relative path that will work cross-platform
|
|
163
|
-
notebook_path = "notebook.ipynb"
|
|
164
|
-
|
|
155
|
+
notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
|
|
156
|
+
app_file_name = AppFileName('test-app-file-name.py')
|
|
165
157
|
|
|
166
|
-
|
|
167
|
-
|
|
158
|
+
# Construct the expected app path using the same method as the production code
|
|
159
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
160
|
+
expected_app_path = get_absolute_app_path(app_directory, app_file_name)
|
|
161
|
+
await streamlit_handler(notebook_path, app_file_name)
|
|
168
162
|
|
|
169
163
|
# Verify calls
|
|
170
164
|
mock_parse.assert_called_once_with(notebook_path)
|
|
171
165
|
mock_generate_code.assert_called_once_with(mock_notebook_data)
|
|
172
166
|
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
|
|
173
|
-
|
|
174
|
-
expected_app_dir = os.path.dirname(os.path.abspath(notebook_path))
|
|
175
|
-
mock_create_file.assert_called_once_with(expected_app_dir, "import streamlit\nst.title('Test')")
|
|
167
|
+
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
|
|
176
168
|
|
|
177
169
|
@pytest.mark.asyncio
|
|
178
170
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
179
171
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
180
172
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
|
|
181
173
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
182
|
-
|
|
174
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_validation_retry')
|
|
175
|
+
async def test_streamlit_handler_max_retries_exceeded(self, mock_log_retry, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
|
|
183
176
|
"""Test streamlit handler when max retries are exceeded"""
|
|
184
177
|
# Mock notebook parsing
|
|
185
178
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -189,16 +182,15 @@ class TestStreamlitHandler:
|
|
|
189
182
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
190
183
|
mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
|
|
191
184
|
|
|
192
|
-
# Mock validation (always errors) -
|
|
193
|
-
mock_validator.return_value =
|
|
185
|
+
# Mock validation (always errors) - validate_app returns List[str]
|
|
186
|
+
mock_validator.return_value = ["Persistent error"]
|
|
194
187
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
assert result[0] is False
|
|
199
|
-
assert "Error generating streamlit code by agent" in result[2]
|
|
188
|
+
# Now it should raise an exception instead of returning a tuple
|
|
189
|
+
with pytest.raises(Exception):
|
|
190
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
200
191
|
|
|
201
|
-
# Verify that error correction was called 5 times (
|
|
192
|
+
# Verify that error correction was called 5 times (once per error, 5 retries)
|
|
193
|
+
# Each retry processes 1 error, so 5 retries = 5 calls
|
|
202
194
|
assert mock_correct_error.call_count == 5
|
|
203
195
|
|
|
204
196
|
@pytest.mark.asyncio
|
|
@@ -206,8 +198,7 @@ class TestStreamlitHandler:
|
|
|
206
198
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
207
199
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
208
200
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
209
|
-
|
|
210
|
-
async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
201
|
+
async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
211
202
|
"""Test streamlit handler when file creation fails"""
|
|
212
203
|
# Mock notebook parsing
|
|
213
204
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -216,38 +207,30 @@ class TestStreamlitHandler:
|
|
|
216
207
|
# Mock code generation
|
|
217
208
|
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
218
209
|
|
|
219
|
-
# Mock validation (no errors)
|
|
220
|
-
mock_validator.return_value =
|
|
221
|
-
|
|
222
|
-
# Mock file creation failure
|
|
223
|
-
mock_create_file.return_value = (False, None, "Permission denied")
|
|
210
|
+
# Mock validation (no errors) - validate_app returns List[str]
|
|
211
|
+
mock_validator.return_value = []
|
|
224
212
|
|
|
225
|
-
# Mock
|
|
226
|
-
|
|
213
|
+
# Mock file creation failure - now it should raise an exception
|
|
214
|
+
mock_create_file.side_effect = Exception("Permission denied")
|
|
227
215
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
assert "Permission denied" in result[2]
|
|
216
|
+
# Now it should raise an exception instead of returning a tuple
|
|
217
|
+
with pytest.raises(Exception):
|
|
218
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
232
219
|
|
|
233
220
|
@pytest.mark.asyncio
|
|
234
221
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
235
|
-
|
|
236
|
-
async def test_streamlit_handler_parse_notebook_exception(self, mock_clean_directory, mock_parse):
|
|
222
|
+
async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
|
|
237
223
|
"""Test streamlit handler when notebook parsing fails"""
|
|
238
|
-
# Mock clean directory check (no-op)
|
|
239
|
-
mock_clean_directory.return_value = None
|
|
240
224
|
|
|
241
225
|
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
242
226
|
|
|
243
227
|
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
244
|
-
await streamlit_handler("notebook.ipynb")
|
|
228
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
245
229
|
|
|
246
230
|
@pytest.mark.asyncio
|
|
247
231
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
248
232
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
249
|
-
|
|
250
|
-
async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generate_code, mock_parse):
|
|
233
|
+
async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
|
|
251
234
|
"""Test streamlit handler when code generation fails"""
|
|
252
235
|
# Mock notebook parsing
|
|
253
236
|
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
@@ -256,11 +239,8 @@ class TestStreamlitHandler:
|
|
|
256
239
|
# Mock code generation failure
|
|
257
240
|
mock_generate_code.side_effect = Exception("Generation failed")
|
|
258
241
|
|
|
259
|
-
# Mock clean directory check (no-op)
|
|
260
|
-
mock_clean_directory.return_value = None
|
|
261
|
-
|
|
262
242
|
with pytest.raises(Exception, match="Generation failed"):
|
|
263
|
-
await streamlit_handler("notebook.ipynb")
|
|
243
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
264
244
|
|
|
265
245
|
|
|
266
246
|
|
|
@@ -11,6 +11,7 @@ from mito_ai.streamlit_conversion.streamlit_utils import (
|
|
|
11
11
|
create_app_file,
|
|
12
12
|
parse_jupyter_notebook_to_extract_required_content
|
|
13
13
|
)
|
|
14
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookDirPath, AbsoluteNotebookPath, get_absolute_notebook_path
|
|
14
15
|
from typing import Dict, Any
|
|
15
16
|
|
|
16
17
|
class TestExtractCodeBlocks:
|
|
@@ -48,55 +49,47 @@ class TestCreateAppFile:
|
|
|
48
49
|
|
|
49
50
|
def test_create_app_file_success(self, tmp_path):
|
|
50
51
|
"""Test successful file creation"""
|
|
51
|
-
|
|
52
|
+
app_path = os.path.join(str(tmp_path), "app.py")
|
|
52
53
|
code = "import streamlit\nst.title('Test')"
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
create_app_file(AbsoluteAppPath(app_path), code)
|
|
55
56
|
|
|
56
|
-
assert
|
|
57
|
-
assert
|
|
57
|
+
assert app_path is not None
|
|
58
|
+
assert os.path.exists(app_path)
|
|
58
59
|
|
|
59
60
|
# Verify file was created with correct content
|
|
60
|
-
|
|
61
|
-
assert os.path.exists(app_file_path)
|
|
62
|
-
|
|
63
|
-
with open(app_file_path, 'r') as f:
|
|
61
|
+
with open(app_path, 'r') as f:
|
|
64
62
|
content = f.read()
|
|
65
63
|
assert content == code
|
|
66
64
|
|
|
67
65
|
def test_create_app_file_io_error(self):
|
|
68
66
|
"""Test file creation with IO error"""
|
|
69
|
-
file_path = "/nonexistent/path/that/should/fail"
|
|
67
|
+
file_path = AbsoluteAppPath("/nonexistent/path/that/should/fail")
|
|
70
68
|
code = "import streamlit"
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert success is False
|
|
75
|
-
assert "Error creating file" in message
|
|
70
|
+
with pytest.raises(Exception):
|
|
71
|
+
create_app_file(file_path, code)
|
|
76
72
|
|
|
77
73
|
@patch('builtins.open', side_effect=Exception("Unexpected error"))
|
|
78
74
|
def test_create_app_file_unexpected_error(self, mock_open):
|
|
79
75
|
"""Test file creation with unexpected error"""
|
|
80
|
-
|
|
76
|
+
app_path = AbsoluteAppPath("/tmp/test")
|
|
81
77
|
code = "import streamlit"
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
assert success is False
|
|
86
|
-
assert "Unexpected error" in message
|
|
79
|
+
with pytest.raises(Exception, match="Unexpected error"):
|
|
80
|
+
create_app_file(app_path, code)
|
|
87
81
|
|
|
88
82
|
def test_create_app_file_empty_code(self, tmp_path):
|
|
89
83
|
"""Test creating file with empty code"""
|
|
90
|
-
|
|
84
|
+
app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
|
|
91
85
|
code = ""
|
|
92
86
|
|
|
93
|
-
|
|
87
|
+
create_app_file(app_path, code)
|
|
94
88
|
|
|
95
|
-
assert
|
|
96
|
-
assert
|
|
89
|
+
assert app_path is not None
|
|
90
|
+
assert os.path.exists(app_path)
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
with open(app_file_path, 'r') as f:
|
|
92
|
+
with open(app_path, 'r') as f:
|
|
100
93
|
content = f.read()
|
|
101
94
|
assert content == ""
|
|
102
95
|
|
|
@@ -128,7 +121,8 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
128
121
|
with open(notebook_path, 'w') as f:
|
|
129
122
|
json.dump(notebook_data, f)
|
|
130
123
|
|
|
131
|
-
|
|
124
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
125
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
132
126
|
|
|
133
127
|
# Check that only cell_type and source are preserved
|
|
134
128
|
assert len(result) == 2
|
|
@@ -143,8 +137,9 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
143
137
|
|
|
144
138
|
def test_parse_notebook_file_not_found(self):
|
|
145
139
|
"""Test parsing non-existent notebook file"""
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
141
|
+
with pytest.raises(StreamlitConversionError, match="Notebook file not found"):
|
|
142
|
+
parse_jupyter_notebook_to_extract_required_content(AbsoluteNotebookPath("/nonexistent/path/notebook.ipynb"))
|
|
148
143
|
|
|
149
144
|
def test_parse_notebook_with_missing_cell_fields(self, tmp_path):
|
|
150
145
|
"""Test parsing notebook where cells are missing cell_type or source"""
|
|
@@ -169,7 +164,8 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
169
164
|
with open(notebook_path, 'w') as f:
|
|
170
165
|
json.dump(notebook_data, f)
|
|
171
166
|
|
|
172
|
-
|
|
167
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
168
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
173
169
|
|
|
174
170
|
assert len(result) == 3
|
|
175
171
|
assert result[0]['cell_type'] == 'code'
|
|
@@ -191,6 +187,7 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
191
187
|
with open(notebook_path, 'w') as f:
|
|
192
188
|
json.dump(notebook_data, f)
|
|
193
189
|
|
|
194
|
-
|
|
190
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
191
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
195
192
|
|
|
196
193
|
assert result == []
|
|
@@ -7,10 +7,10 @@ from unittest.mock import patch, MagicMock
|
|
|
7
7
|
from mito_ai.streamlit_conversion.validate_streamlit_app import (
|
|
8
8
|
get_syntax_error,
|
|
9
9
|
get_runtime_errors,
|
|
10
|
-
check_for_errors,
|
|
11
10
|
validate_app
|
|
12
11
|
)
|
|
13
12
|
import pytest
|
|
13
|
+
from mito_ai.path_utils import AbsoluteNotebookPath
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TestGetSyntaxError:
|
|
@@ -19,33 +19,33 @@ class TestGetSyntaxError:
|
|
|
19
19
|
@pytest.mark.parametrize("code,expected_error,test_description", [
|
|
20
20
|
# Valid Python code should return no error
|
|
21
21
|
(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
"import streamlit\nst.title('Hello World')",
|
|
23
|
+
None,
|
|
24
|
+
"valid Python code"
|
|
25
25
|
),
|
|
26
26
|
# Invalid Python syntax should be caught
|
|
27
27
|
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
"import streamlit\nst.title('Hello World'",
|
|
29
|
+
"SyntaxError",
|
|
30
|
+
"invalid Python code"
|
|
31
31
|
),
|
|
32
32
|
# Empty streamlit app is valid
|
|
33
33
|
(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"",
|
|
35
|
+
None,
|
|
36
|
+
"empty code"
|
|
37
37
|
),
|
|
38
38
|
])
|
|
39
39
|
def test_get_syntax_error(self, code, expected_error, test_description):
|
|
40
40
|
"""Test syntax validation with various code inputs"""
|
|
41
41
|
error = get_syntax_error(code)
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
if expected_error is None:
|
|
44
44
|
assert error is None, f"Expected no error for {test_description}"
|
|
45
45
|
else:
|
|
46
46
|
assert error is not None, f"Expected error for {test_description}"
|
|
47
47
|
assert expected_error in error, f"Expected '{expected_error}' in error for {test_description}"
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
class TestGetRuntimeErrors:
|
|
50
50
|
"""Test cases for get_runtime_errors function"""
|
|
51
51
|
|
|
@@ -56,7 +56,9 @@ class TestGetRuntimeErrors:
|
|
|
56
56
|
])
|
|
57
57
|
def test_get_runtime_errors(self, app_code, expected_error):
|
|
58
58
|
"""Test getting runtime errors"""
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
absolute_path = AbsoluteNotebookPath('/notebook.ipynb')
|
|
61
|
+
errors = get_runtime_errors(app_code, absolute_path)
|
|
60
62
|
|
|
61
63
|
if expected_error is None:
|
|
62
64
|
assert errors is None
|
|
@@ -85,23 +87,26 @@ df=pd.read_csv('data.csv')
|
|
|
85
87
|
with open(csv_path, "w") as f:
|
|
86
88
|
f.write("name,age\nJohn,25\nJane,30")
|
|
87
89
|
|
|
88
|
-
errors = get_runtime_errors(app_code, app_path)
|
|
90
|
+
errors = get_runtime_errors(app_code, AbsoluteNotebookPath(app_path))
|
|
89
91
|
assert errors is None
|
|
90
92
|
|
|
91
93
|
class TestValidateApp:
|
|
92
94
|
"""Test cases for validate_app function"""
|
|
93
95
|
|
|
94
|
-
@pytest.mark.parametrize("app_code,
|
|
96
|
+
@pytest.mark.parametrize("app_code,expected_has_errors,expected_error_message", [
|
|
95
97
|
("x=5", False, ""),
|
|
96
98
|
("1/0", True, "division by zero"),
|
|
97
|
-
|
|
99
|
+
# Syntax errors are caught during runtime
|
|
98
100
|
("", False, ""),
|
|
99
101
|
])
|
|
100
|
-
def test_validate_app(self, app_code,
|
|
102
|
+
def test_validate_app(self, app_code, expected_has_errors, expected_error_message):
|
|
101
103
|
"""Test the validate_app function"""
|
|
102
|
-
|
|
104
|
+
errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
assert
|
|
106
|
+
has_errors = len(errors) > 0
|
|
107
|
+
assert has_errors == expected_has_errors
|
|
108
|
+
if expected_error_message:
|
|
109
|
+
errors_str = str(errors)
|
|
110
|
+
assert expected_error_message in errors_str
|
|
106
111
|
|
|
107
112
|
|
|
@@ -4,85 +4,115 @@
|
|
|
4
4
|
import pytest
|
|
5
5
|
import os
|
|
6
6
|
import tempfile
|
|
7
|
-
from unittest.mock import patch, AsyncMock, MagicMock
|
|
8
|
-
from mito_ai.streamlit_preview.
|
|
7
|
+
from unittest.mock import patch, Mock, AsyncMock, MagicMock
|
|
8
|
+
from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
|
|
9
|
+
from mito_ai.path_utils import AbsoluteNotebookPath
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class
|
|
12
|
-
"""Test cases for the
|
|
12
|
+
class TestStreamlitPreviewHandler:
|
|
13
|
+
"""Test cases for the StreamlitPreviewHandler."""
|
|
13
14
|
|
|
14
15
|
@pytest.mark.asyncio
|
|
15
16
|
@pytest.mark.parametrize(
|
|
16
|
-
"app_exists,
|
|
17
|
+
"app_exists,force_recreate,streamlit_handler_called",
|
|
17
18
|
[
|
|
18
|
-
# Test case 1: App exists, should
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
False, # streamlit_handler_called
|
|
25
|
-
None, # streamlit_handler_return (not used)
|
|
26
|
-
),
|
|
27
|
-
# Test case 2: App doesn't exist, streamlit_handler succeeds
|
|
28
|
-
(
|
|
29
|
-
False, # app_exists
|
|
30
|
-
True, # streamlit_handler_success
|
|
31
|
-
True, # expected_success
|
|
32
|
-
"", # expected_error
|
|
33
|
-
True, # streamlit_handler_called
|
|
34
|
-
(True, "/path/to/app.py", "Success"), # streamlit_handler_return
|
|
35
|
-
)
|
|
19
|
+
# Test case 1: App exists, not forcing recreate - should not call streamlit_handler
|
|
20
|
+
(True, False, False),
|
|
21
|
+
# Test case 2: App doesn't exist - should call streamlit_handler
|
|
22
|
+
(False, False, True),
|
|
23
|
+
# Test case 3: App exists but forcing recreate - should call streamlit_handler
|
|
24
|
+
(True, True, True),
|
|
36
25
|
],
|
|
37
26
|
ids=[
|
|
38
|
-
"
|
|
39
|
-
"
|
|
27
|
+
"app_exists_no_force_recreate",
|
|
28
|
+
"app_does_not_exist_generates_new_one",
|
|
29
|
+
"app_exists_force_recreate",
|
|
40
30
|
]
|
|
41
31
|
)
|
|
42
|
-
async def
|
|
32
|
+
async def test_post_handler_app_generation(
|
|
43
33
|
self,
|
|
44
34
|
app_exists,
|
|
45
|
-
|
|
46
|
-
expected_success,
|
|
47
|
-
expected_error,
|
|
35
|
+
force_recreate,
|
|
48
36
|
streamlit_handler_called,
|
|
49
|
-
streamlit_handler_return,
|
|
50
37
|
):
|
|
51
|
-
"""Test
|
|
38
|
+
"""Test StreamlitPreviewHandler POST method with various scenarios."""
|
|
52
39
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
53
40
|
notebook_path = os.path.join(temp_dir, "test_notebook.ipynb")
|
|
41
|
+
notebook_id = "test_notebook_id"
|
|
42
|
+
# App file name is derived from notebook_id
|
|
43
|
+
app_file_name = f"{notebook_id}.py"
|
|
44
|
+
app_path = os.path.join(temp_dir, app_file_name)
|
|
54
45
|
|
|
55
|
-
#
|
|
56
|
-
|
|
46
|
+
# Create notebook file
|
|
47
|
+
with open(notebook_path, "w") as f:
|
|
48
|
+
f.write('{"cells": []}')
|
|
57
49
|
|
|
58
|
-
# Create app
|
|
50
|
+
# Create app file if it should exist
|
|
59
51
|
if app_exists:
|
|
60
|
-
assert app_path is not None # Type assertion for mypy
|
|
61
52
|
with open(app_path, "w") as f:
|
|
62
53
|
f.write("import streamlit as st\nst.write('Hello World')")
|
|
63
54
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
# Create a properly mocked Tornado application with required attributes
|
|
56
|
+
mock_application = MagicMock()
|
|
57
|
+
mock_application.ui_methods = {}
|
|
58
|
+
mock_application.ui_modules = {}
|
|
59
|
+
mock_application.settings = {}
|
|
60
|
+
|
|
61
|
+
# Create a mock request with necessary tornado setup
|
|
62
|
+
mock_request = MagicMock()
|
|
63
|
+
mock_request.connection = MagicMock()
|
|
64
|
+
mock_request.connection.context = MagicMock()
|
|
65
|
+
|
|
66
|
+
# Create handler instance
|
|
67
|
+
handler = StreamlitPreviewHandler(
|
|
68
|
+
application=mock_application,
|
|
69
|
+
request=mock_request,
|
|
70
|
+
)
|
|
71
|
+
handler.initialize()
|
|
72
|
+
|
|
73
|
+
# Mock authentication - set current_user to bypass @tornado.web.authenticated
|
|
74
|
+
handler.current_user = "test_user" # type: ignore
|
|
75
|
+
|
|
76
|
+
# Mock the finish method and other handler methods
|
|
77
|
+
finish_called = []
|
|
78
|
+
def mock_finish_func(response):
|
|
79
|
+
finish_called.append(response)
|
|
80
|
+
|
|
81
|
+
# Mock streamlit_handler and preview manager
|
|
82
|
+
with patch.object(handler, 'get_json_body', return_value={
|
|
83
|
+
"notebook_path": notebook_path,
|
|
84
|
+
"notebook_id": notebook_id,
|
|
85
|
+
"force_recreate": force_recreate,
|
|
86
|
+
"edit_prompt": ""
|
|
87
|
+
}), \
|
|
88
|
+
patch.object(handler, 'finish', side_effect=mock_finish_func), \
|
|
89
|
+
patch.object(handler, 'set_status'), \
|
|
90
|
+
patch('mito_ai.streamlit_preview.handlers.streamlit_handler', new_callable=AsyncMock) as mock_streamlit_handler, \
|
|
91
|
+
patch.object(handler.preview_manager, 'start_streamlit_preview', return_value=8501) as mock_start_preview, \
|
|
92
|
+
patch('mito_ai.streamlit_preview.handlers.log_streamlit_app_preview_success'):
|
|
93
|
+
|
|
94
|
+
# Call the handler
|
|
95
|
+
await handler.post() # type: ignore[misc]
|
|
96
|
+
|
|
97
|
+
# Verify streamlit_handler was called or not called as expected
|
|
98
|
+
if streamlit_handler_called:
|
|
99
|
+
assert mock_streamlit_handler.called
|
|
100
|
+
# Verify it was called with the correct arguments
|
|
101
|
+
call_args = mock_streamlit_handler.call_args
|
|
102
|
+
assert call_args[0][0] == os.path.abspath(notebook_path) # First argument should be the absolute notebook path
|
|
103
|
+
assert call_args[0][1] == app_file_name # Second argument should be the app file name
|
|
104
|
+
assert call_args[0][2] == "" # Third argument should be the edit_prompt
|
|
105
|
+
else:
|
|
106
|
+
mock_streamlit_handler.assert_not_called()
|
|
107
|
+
|
|
108
|
+
# Verify preview was started
|
|
109
|
+
mock_start_preview.assert_called_once()
|
|
67
110
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Assertions
|
|
76
|
-
assert success == expected_success
|
|
77
|
-
assert error_msg == expected_error
|
|
78
|
-
|
|
79
|
-
# Verify get_app_path was called with the correct directory
|
|
80
|
-
mock_get_app_path.assert_called_once_with(temp_dir)
|
|
81
|
-
|
|
82
|
-
# Verify streamlit_handler was called or not called as expected
|
|
83
|
-
if streamlit_handler_called:
|
|
84
|
-
mock_streamlit_handler.assert_called_once_with(notebook_path, "")
|
|
85
|
-
else:
|
|
86
|
-
mock_streamlit_handler.assert_not_called()
|
|
111
|
+
# Verify response was sent
|
|
112
|
+
assert len(finish_called) == 1
|
|
113
|
+
response = finish_called[0]
|
|
114
|
+
assert response["type"] == "success"
|
|
115
|
+
assert "port" in response
|
|
116
|
+
assert "id" in response
|
|
87
117
|
|
|
88
118
|
|