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.
Files changed (74) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/app_deploy_utils.py +28 -9
  3. mito_ai/app_deploy/handlers.py +123 -84
  4. mito_ai/app_deploy/models.py +19 -12
  5. mito_ai/completions/models.py +6 -1
  6. mito_ai/completions/prompt_builders/agent_execution_prompt.py +13 -1
  7. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  8. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  9. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  10. mito_ai/completions/prompt_builders/utils.py +13 -0
  11. mito_ai/path_utils.py +70 -0
  12. mito_ai/streamlit_conversion/agent_utils.py +4 -201
  13. mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
  14. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  15. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
  16. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
  17. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  18. mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
  19. mito_ai/streamlit_conversion/streamlit_utils.py +13 -75
  20. mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
  21. mito_ai/streamlit_preview/__init__.py +1 -2
  22. mito_ai/streamlit_preview/handlers.py +54 -85
  23. mito_ai/streamlit_preview/manager.py +11 -18
  24. mito_ai/streamlit_preview/utils.py +12 -28
  25. mito_ai/tests/deploy_app/test_app_deploy_utils.py +22 -4
  26. mito_ai/tests/message_history/test_message_history_utils.py +3 -0
  27. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  28. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +40 -60
  29. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  30. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
  31. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +87 -57
  32. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +27 -40
  33. mito_ai/user/handlers.py +15 -3
  34. mito_ai/utils/create.py +17 -1
  35. mito_ai/utils/error_classes.py +42 -0
  36. mito_ai/utils/message_history_utils.py +3 -1
  37. mito_ai/utils/telemetry_utils.py +78 -13
  38. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  39. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  40. {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
  41. 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
  42. mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
  43. 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
  44. 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
  45. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/METADATA +1 -1
  46. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/RECORD +71 -69
  47. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  48. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  49. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  50. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  51. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  52. {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
  53. {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
  54. {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
  55. {mito_ai-0.1.46.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
  73. {mito_ai-0.1.46.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
  74. {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.streamlit_conversion.streamlit_utils import clean_directory_check
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 = """```unified_diff
106
- --- a/app.py
107
- +++ b/app.py
108
- @@ -1,1 +1,1 @@
109
- -import streamlit
110
- -st.title('Test')
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.clean_directory_check')
144
- async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
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 = (False, "")
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
- result = await streamlit_handler(notebook_path)
155
+ notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
156
+ app_file_name = AppFileName('test-app-file-name.py')
165
157
 
166
- assert result[0] is True
167
- assert "File created successfully" in result[2]
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
- # get_app_directory converts relative paths to absolute, so expect the absolute path directory
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
- async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
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) - Return list of errors as expected by validate_app
193
- mock_validator.return_value = (True, ["Persistent error"])
185
+ # Mock validation (always errors) - validate_app returns List[str]
186
+ mock_validator.return_value = ["Persistent error"]
194
187
 
195
- result = await streamlit_handler("notebook.ipynb")
196
-
197
- # Verify the result indicates failure
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 (max retries)
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
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
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 = (False, "")
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 clean directory check (no-op)
226
- mock_clean_directory.return_value = None
213
+ # Mock file creation failure - now it should raise an exception
214
+ mock_create_file.side_effect = Exception("Permission denied")
227
215
 
228
- result = await streamlit_handler("notebook.ipynb")
229
-
230
- assert result[0] is False
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
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
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
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
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
- file_path = str(tmp_path)
52
+ app_path = os.path.join(str(tmp_path), "app.py")
52
53
  code = "import streamlit\nst.title('Test')"
53
54
 
54
- success, app_path, message = create_app_file(file_path, code)
55
+ create_app_file(AbsoluteAppPath(app_path), code)
55
56
 
56
- assert success is True
57
- assert "Successfully created" in message
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
- app_file_path = os.path.join(file_path, "app.py")
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
- success, app_path, message = create_app_file(file_path, code)
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
- file_path = "/tmp/test"
76
+ app_path = AbsoluteAppPath("/tmp/test")
81
77
  code = "import streamlit"
82
78
 
83
- success, app_path, message = create_app_file(file_path, code)
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
- file_path = str(tmp_path)
84
+ app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
91
85
  code = ""
92
86
 
93
- success, app_path, message = create_app_file(file_path, code)
87
+ create_app_file(app_path, code)
94
88
 
95
- assert success is True
96
- assert "Successfully created" in message
89
+ assert app_path is not None
90
+ assert os.path.exists(app_path)
97
91
 
98
- app_file_path = os.path.join(file_path, "app.py")
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
- result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
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
- with pytest.raises(FileNotFoundError, match="Notebook file not found"):
147
- parse_jupyter_notebook_to_extract_required_content("/nonexistent/path/notebook.ipynb")
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
- result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
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
- result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
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
- "import streamlit\nst.title('Hello World')",
23
- None,
24
- "valid Python code"
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
- "import streamlit\nst.title('Hello World'",
29
- "SyntaxError",
30
- "invalid Python code"
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
- None,
36
- "empty code"
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
- errors = get_runtime_errors(app_code, '/app.py')
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,expected_has_validation_error,expected_error_message", [
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
- ("print('Hello World'", True, "SyntaxError"),
99
+ # Syntax errors are caught during runtime
98
100
  ("", False, ""),
99
101
  ])
100
- def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
102
+ def test_validate_app(self, app_code, expected_has_errors, expected_error_message):
101
103
  """Test the validate_app function"""
102
- has_validation_error, errors = validate_app(app_code, '/app.py')
104
+ errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
103
105
 
104
- assert has_validation_error == expected_has_validation_error
105
- assert expected_error_message in str(errors)
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.utils import ensure_app_exists
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 TestEnsureAppExists:
12
- """Test cases for the ensure_app_exists function."""
12
+ class TestStreamlitPreviewHandler:
13
+ """Test cases for the StreamlitPreviewHandler."""
13
14
 
14
15
  @pytest.mark.asyncio
15
16
  @pytest.mark.parametrize(
16
- "app_exists,streamlit_handler_success,expected_success,expected_error,streamlit_handler_called,streamlit_handler_return",
17
+ "app_exists,force_recreate,streamlit_handler_called",
17
18
  [
18
- # Test case 1: App exists, should use existing file
19
- (
20
- True, # app_exists
21
- True, # streamlit_handler_success (not relevant)
22
- True, # expected_success
23
- "", # expected_error
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
- "app_exists_uses_existing_file",
39
- "app_does_not_exist_generates_new_one_success",
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 test_ensure_app_exists(
32
+ async def test_post_handler_app_generation(
43
33
  self,
44
34
  app_exists,
45
- streamlit_handler_success,
46
- expected_success,
47
- expected_error,
35
+ force_recreate,
48
36
  streamlit_handler_called,
49
- streamlit_handler_return,
50
37
  ):
51
- """Test ensure_app_exists function with various scenarios."""
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
- # Set up app_path based on whether app exists
56
- app_path = os.path.join(temp_dir, "app.py") if app_exists else None
46
+ # Create notebook file
47
+ with open(notebook_path, "w") as f:
48
+ f.write('{"cells": []}')
57
49
 
58
- # Create app.py file if it should exist
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
- # Mock get_app_path to return the appropriate value
65
- with patch('mito_ai.streamlit_preview.utils.get_app_path') as mock_get_app_path:
66
- mock_get_app_path.return_value = app_path
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
- # Mock streamlit_handler
69
- with patch('mito_ai.streamlit_preview.utils.streamlit_handler') as mock_streamlit_handler:
70
- if streamlit_handler_return is not None:
71
- mock_streamlit_handler.return_value = streamlit_handler_return
72
-
73
- success, error_msg = await ensure_app_exists(notebook_path, False, "")
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