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.

Files changed (56) hide show
  1. mito_ai/__init__.py +17 -1
  2. mito_ai/_version.py +1 -1
  3. mito_ai/app_builder/handlers.py +43 -38
  4. mito_ai/app_builder/models.py +1 -1
  5. mito_ai/completions/handlers.py +1 -1
  6. mito_ai/completions/prompt_builders/agent_system_message.py +18 -45
  7. mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
  8. mito_ai/log/handlers.py +10 -3
  9. mito_ai/log/urls.py +3 -3
  10. mito_ai/openai_client.py +1 -1
  11. mito_ai/streamlit_conversion/agent_utils.py +116 -0
  12. mito_ai/streamlit_conversion/prompts/prompt_constants.py +59 -0
  13. mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
  14. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +45 -0
  15. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
  16. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +44 -0
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +90 -44
  18. mito_ai/streamlit_conversion/streamlit_system_prompt.py +30 -17
  19. mito_ai/streamlit_conversion/streamlit_utils.py +48 -8
  20. mito_ai/streamlit_conversion/validate_streamlit_app.py +116 -0
  21. mito_ai/streamlit_preview/__init__.py +7 -0
  22. mito_ai/streamlit_preview/handlers.py +164 -0
  23. mito_ai/streamlit_preview/manager.py +159 -0
  24. mito_ai/streamlit_preview/urls.py +22 -0
  25. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +166 -78
  26. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +4 -5
  27. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +119 -0
  28. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +302 -0
  29. mito_ai/tests/utils/test_anthropic_utils.py +2 -2
  30. mito_ai/utils/anthropic_utils.py +4 -4
  31. mito_ai/utils/open_ai_utils.py +0 -4
  32. mito_ai/utils/telemetry_utils.py +28 -1
  33. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  34. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  35. {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
  36. {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
  37. 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
  38. mito_ai-0.1.39.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.16b532b655cd2906e04a.js.map +1 -0
  39. 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
  40. 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
  41. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/METADATA +4 -1
  42. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/RECORD +53 -42
  43. mito_ai/streamlit_conversion/validate_and_run_streamlit_code.py +0 -207
  44. mito_ai/tests/streamlit_conversion/test_validate_and_run_streamlit_code.py +0 -418
  45. mito_ai-0.1.37.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.831f63b48760c7119b9b.js.map +0 -1
  46. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  47. {mito_ai-0.1.37.data → mito_ai-0.1.39.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  48. {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
  49. {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
  50. {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
  51. {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
  52. {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
  53. {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
  54. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/WHEEL +0 -0
  55. {mito_ai-0.1.37.dist-info → mito_ai-0.1.39.dist-info}/entry_points.txt +0 -0
  56. {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 typing import cast
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
- def test_init(self):
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
- generator = StreamlitCodeGeneration(notebook_data)
23
-
24
- assert len(generator.messages) == 1
25
- assert generator.messages[0]["role"] == "user"
26
- # Access content properly as a list and cast to expected type
27
- content_list = cast(list, generator.messages[0]["content"])
28
- assert isinstance(content_list, list)
29
- assert len(content_list) > 0
30
- content_item = cast(dict, content_list[0])
31
- assert content_item["type"] == "text"
32
- assert "jupyter notebook content" in content_item["text"]
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
- generator = StreamlitCodeGeneration(notebook_data)
55
+ streamlit_code_handler = StreamlitCodeGeneration()
52
56
 
53
- result = await generator.get_response_from_agent(generator.messages)
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
- generator = StreamlitCodeGeneration(notebook_data)
70
+ streamlit_code_handler = StreamlitCodeGeneration()
67
71
 
68
72
  with pytest.raises(Exception, match="API Error"):
69
- await generator.get_response_from_agent(generator.messages)
70
-
71
- def test_add_agent_response_to_context(self):
72
- """Test adding agent response to message history"""
73
- notebook_data: dict = {"cells": []}
74
- generator = StreamlitCodeGeneration(notebook_data)
75
-
76
- initial_count = len(generator.messages)
77
- generator.add_agent_response_to_context("Test response")
78
-
79
- assert len(generator.messages) == initial_count + 1
80
- assert generator.messages[-1]["role"] == "assistant"
81
- # Access content properly as a list and cast to expected type
82
- content_list = cast(list, generator.messages[-1]["content"])
83
- assert isinstance(content_list, list)
84
- assert len(content_list) > 0
85
- content_item = cast(dict, content_list[0])
86
- assert content_item["type"] == "text"
87
- assert content_item["text"] == "Test response"
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
- generator = StreamlitCodeGeneration(notebook_data)
88
+ streamlit_code_handler = StreamlitCodeGeneration()
103
89
 
104
- result = await generator.generate_streamlit_code()
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 = "Here's the corrected code:\n```python\nimport streamlit\nst.title('Fixed')\n```"
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
- generator = StreamlitCodeGeneration(notebook_data)
115
+ streamlit_code_handler = StreamlitCodeGeneration()
126
116
 
127
- result = await generator.correct_error_in_generation("ImportError: No module named 'pandas'")
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
- notebook_data: dict = {"cells": []}
144
- generator = StreamlitCodeGeneration(notebook_data)
128
+ streamlit_code_handler = StreamlitCodeGeneration()
145
129
 
146
130
  with pytest.raises(Exception, match="API Error"):
147
- await generator.correct_error_in_generation("Some error")
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.streamlit_code_validator')
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
- async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generator_class, mock_parse):
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", "/path/to/app")
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[1]
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(mock_notebook_data)
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/app", "import streamlit\nst.title('Test')")
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.streamlit_code_validator')
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
- # Mock validation (always errors)
204
- mock_validator.return_value = (True, "Persistent error")
205
-
206
- result = await streamlit_handler("/path/to/notebook.ipynb", "/path/to/app")
207
-
196
+ # Verify the result indicates failure
208
197
  assert result[0] is False
209
- assert "Error generating streamlit code by agent" in result[1]
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.streamlit_code_validator')
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
- async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generator_class, mock_parse):
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", "/path/to/app")
229
+ result = await streamlit_handler("/path/to/notebook.ipynb")
237
230
 
238
231
  assert result[0] is False
239
- assert "Permission denied" in result[1]
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
- async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
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", "/path/to/app")
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
- async def test_streamlit_handler_generation_exception(self, mock_generator_class, mock_parse):
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", "/path/to/app")
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
+