mito-ai 0.1.44__py3-none-any.whl → 0.1.46__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 (76) hide show
  1. mito_ai/__init__.py +10 -1
  2. mito_ai/_version.py +1 -1
  3. mito_ai/anthropic_client.py +92 -8
  4. mito_ai/app_deploy/app_deploy_utils.py +25 -0
  5. mito_ai/app_deploy/handlers.py +9 -12
  6. mito_ai/app_deploy/models.py +4 -1
  7. mito_ai/chat_history/handlers.py +63 -0
  8. mito_ai/chat_history/urls.py +32 -0
  9. mito_ai/completions/handlers.py +44 -20
  10. mito_ai/completions/models.py +1 -0
  11. mito_ai/completions/prompt_builders/prompt_constants.py +22 -4
  12. mito_ai/constants.py +3 -0
  13. mito_ai/streamlit_conversion/agent_utils.py +148 -30
  14. mito_ai/streamlit_conversion/prompts/prompt_constants.py +147 -24
  15. mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
  16. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +2 -2
  17. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
  18. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
  19. mito_ai/streamlit_conversion/streamlit_agent_handler.py +101 -104
  20. mito_ai/streamlit_conversion/streamlit_system_prompt.py +1 -0
  21. mito_ai/streamlit_conversion/streamlit_utils.py +18 -17
  22. mito_ai/streamlit_conversion/validate_streamlit_app.py +66 -62
  23. mito_ai/streamlit_preview/handlers.py +5 -3
  24. mito_ai/streamlit_preview/utils.py +11 -7
  25. mito_ai/tests/chat_history/test_chat_history.py +211 -0
  26. mito_ai/tests/deploy_app/test_app_deploy_utils.py +71 -0
  27. mito_ai/tests/message_history/test_message_history_utils.py +43 -19
  28. mito_ai/tests/providers/test_anthropic_client.py +180 -8
  29. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +368 -0
  30. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +533 -0
  31. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +71 -158
  32. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +16 -16
  33. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +16 -28
  34. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +2 -2
  35. mito_ai/tests/user/__init__.py +2 -0
  36. mito_ai/tests/user/test_user.py +120 -0
  37. mito_ai/tests/utils/test_anthropic_utils.py +4 -4
  38. mito_ai/user/handlers.py +33 -0
  39. mito_ai/user/urls.py +21 -0
  40. mito_ai/utils/anthropic_utils.py +15 -21
  41. mito_ai/utils/message_history_utils.py +4 -3
  42. mito_ai/utils/telemetry_utils.py +7 -4
  43. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  44. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  45. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  46. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js +1520 -300
  47. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +1 -0
  48. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5482493d1270f55b7283.js → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js +18 -18
  49. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.5482493d1270f55b7283.js.map → mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map +1 -1
  50. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/METADATA +2 -2
  51. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/RECORD +75 -63
  52. mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js.map +0 -1
  53. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  54. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  55. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  56. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  57. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  58. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  59. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  60. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  61. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  62. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  63. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  64. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +0 -0
  65. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +0 -0
  66. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +0 -0
  67. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +0 -0
  68. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  69. {mito_ai-0.1.44.data → mito_ai-0.1.46.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
  70. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  71. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
  72. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
  73. {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
  74. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/WHEEL +0 -0
  75. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/entry_points.txt +0 -0
  76. {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/licenses/LICENSE +0 -0
@@ -1,10 +1,15 @@
1
1
  # Copyright (c) Saga Inc.
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
+ from typing import List
5
+ from anthropic.types import MessageParam
4
6
  import pytest
7
+ import os
5
8
  from unittest.mock import patch, AsyncMock, MagicMock
6
9
  from mito_ai.streamlit_conversion.streamlit_agent_handler import (
7
- StreamlitCodeGeneration,
10
+ get_response_from_agent,
11
+ generate_new_streamlit_code,
12
+ correct_error_in_generation,
8
13
  streamlit_handler
9
14
  )
10
15
  from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
@@ -13,36 +18,34 @@ from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
13
18
  pytest_plugins = ('pytest_asyncio',)
14
19
 
15
20
 
16
- class TestStreamlitCodeGeneration:
17
- """Test cases for StreamlitCodeGeneration class"""
21
+ class TestGetResponseFromAgent:
22
+ """Test cases for get_response_from_agent function"""
18
23
 
19
24
  @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):
22
- """Test StreamlitCodeGeneration initialization"""
25
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
26
+ async def test_get_response_from_agent_success(self, mock_stream):
27
+ """Test get_response_from_agent with successful response"""
23
28
  # Mock the async generator
24
29
  async def mock_async_gen():
25
30
  yield "Here's your code:\n```python\nimport streamlit\nst.title('Test')\n```"
26
31
 
27
32
  mock_stream.return_value = mock_async_gen()
28
33
 
29
- notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
30
- streamlit_code_handler = StreamlitCodeGeneration()
34
+ messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
35
+ response = await get_response_from_agent(messages)
31
36
 
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
37
+ assert response is not None
38
+ assert len(response) > 0
39
+ assert "import streamlit" in response
37
40
 
38
41
  @pytest.mark.asyncio
39
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
42
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
40
43
  @pytest.mark.parametrize("mock_items,expected_result", [
41
44
  (["Hello", " World", "!"], "Hello World!"),
42
45
  ([], ""),
43
46
  (["Here's your code: import streamlit"], "Here's your code: import streamlit")
44
47
  ])
45
- async def test_get_response_from_agent(self, mock_stream, mock_items, expected_result):
48
+ async def test_get_response_from_agent_parametrized(self, mock_stream, mock_items, expected_result):
46
49
  """Test response from agent with different scenarios"""
47
50
  # Mock the async generator
48
51
  async def mock_async_gen():
@@ -51,30 +54,31 @@ class TestStreamlitCodeGeneration:
51
54
 
52
55
  mock_stream.return_value = mock_async_gen()
53
56
 
54
- notebook_data: dict = {"cells": []}
55
- streamlit_code_handler = StreamlitCodeGeneration()
56
-
57
- result = await streamlit_code_handler.generate_streamlit_code(notebook_data)
57
+ messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
58
+ result = await get_response_from_agent(messages)
58
59
 
59
60
  assert result == expected_result
60
61
  mock_stream.assert_called_once()
61
62
 
62
63
 
63
64
  @pytest.mark.asyncio
64
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
65
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
65
66
  async def test_get_response_from_agent_exception(self, mock_stream):
66
67
  """Test exception handling in get_response_from_agent"""
67
68
  mock_stream.side_effect = Exception("API Error")
68
69
 
69
- notebook_data: dict = {"cells": []}
70
- streamlit_code_handler = StreamlitCodeGeneration()
70
+ messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
71
71
 
72
72
  with pytest.raises(Exception, match="API Error"):
73
- await streamlit_code_handler.generate_streamlit_code(notebook_data)
73
+ await get_response_from_agent(messages)
74
+
75
+
76
+ class TestGenerateStreamlitCode:
77
+ """Test cases for generate_new_streamlit_code function"""
74
78
 
75
79
  @pytest.mark.asyncio
76
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
77
- async def test_generate_streamlit_code_success(self, mock_stream):
80
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
81
+ async def test_generate_new_streamlit_code_success(self, mock_stream):
78
82
  """Test successful streamlit code generation"""
79
83
  mock_response = "Here's your code:\n```python\nimport streamlit\nst.title('Hello')\n```"
80
84
 
@@ -84,16 +88,18 @@ class TestStreamlitCodeGeneration:
84
88
 
85
89
  mock_stream.return_value = mock_async_gen()
86
90
 
87
- notebook_data: dict = {"cells": []}
88
- streamlit_code_handler = StreamlitCodeGeneration()
89
-
90
- result = await streamlit_code_handler.generate_streamlit_code(notebook_data)
91
+ notebook_data: List[dict] = [{"cells": []}]
92
+ result = await generate_new_streamlit_code(notebook_data)
91
93
 
92
94
  expected_code = "import streamlit\nst.title('Hello')\n"
93
95
  assert result == expected_code
94
96
 
97
+
98
+ class TestCorrectErrorInGeneration:
99
+ """Test cases for correct_error_in_generation function"""
100
+
95
101
  @pytest.mark.asyncio
96
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
102
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
97
103
  async def test_correct_error_in_generation_success(self, mock_stream):
98
104
  """Test successful error correction"""
99
105
  mock_response = """```unified_diff
@@ -111,24 +117,19 @@ class TestStreamlitCodeGeneration:
111
117
 
112
118
  mock_stream.return_value = mock_async_gen()
113
119
 
114
- notebook_data: dict = {"cells": []}
115
- streamlit_code_handler = StreamlitCodeGeneration()
116
-
117
- result = await streamlit_code_handler.correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
120
+ result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
118
121
 
119
122
  expected_code = "import streamlit\nst.title('Fixed')\n"
120
123
  assert result == expected_code
121
124
 
122
125
  @pytest.mark.asyncio
123
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.stream_anthropic_completion_from_mito_server')
126
+ @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
124
127
  async def test_correct_error_in_generation_exception(self, mock_stream):
125
128
  """Test exception handling in error correction"""
126
129
  mock_stream.side_effect = Exception("API Error")
127
130
 
128
- streamlit_code_handler = StreamlitCodeGeneration()
129
-
130
131
  with pytest.raises(Exception, match="API Error"):
131
- await streamlit_code_handler.correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
132
+ await correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
132
133
 
133
134
 
134
135
  class TestStreamlitHandler:
@@ -136,20 +137,18 @@ class TestStreamlitHandler:
136
137
 
137
138
  @pytest.mark.asyncio
138
139
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
139
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
140
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
140
141
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
141
142
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
142
143
  @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):
144
+ async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
144
145
  """Test successful streamlit handler execution"""
145
146
  # Mock notebook parsing
146
- mock_notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
147
+ mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
147
148
  mock_parse.return_value = mock_notebook_data
148
149
 
149
150
  # Mock code generation
150
- mock_generator = AsyncMock()
151
- mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
152
- mock_generator_class.return_value = mock_generator
151
+ mock_generate_code.return_value = "import streamlit\nst.title('Test')"
153
152
 
154
153
  # Mock validation (no errors)
155
154
  mock_validator.return_value = (False, "")
@@ -160,62 +159,62 @@ class TestStreamlitHandler:
160
159
  # Mock clean directory check (no-op)
161
160
  mock_clean_directory.return_value = None
162
161
 
163
- result = await streamlit_handler("/path/to/notebook.ipynb")
162
+ # Use a relative path that will work cross-platform
163
+ notebook_path = "notebook.ipynb"
164
+ result = await streamlit_handler(notebook_path)
164
165
 
165
166
  assert result[0] is True
166
167
  assert "File created successfully" in result[2]
167
168
 
168
169
  # Verify calls
169
- mock_parse.assert_called_once_with("/path/to/notebook.ipynb")
170
- mock_generator_class.assert_called_once_with()
171
- mock_generator.generate_streamlit_code.assert_called_once()
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')")
170
+ mock_parse.assert_called_once_with(notebook_path)
171
+ mock_generate_code.assert_called_once_with(mock_notebook_data)
172
+ 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')")
174
176
 
175
177
  @pytest.mark.asyncio
176
178
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
177
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
179
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
180
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
178
181
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
179
- async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_generator_class, mock_parse):
182
+ async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
180
183
  """Test streamlit handler when max retries are exceeded"""
181
184
  # Mock notebook parsing
182
- mock_notebook_data: dict = {"cells": []}
185
+ mock_notebook_data: List[dict] = [{"cells": []}]
183
186
  mock_parse.return_value = mock_notebook_data
184
187
 
185
188
  # Mock code generation
186
- mock_generator = AsyncMock()
187
- mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
188
- mock_generator.correct_error_in_generation.return_value = "import streamlit\nst.title('Fixed')"
189
- mock_generator_class.return_value = mock_generator
189
+ mock_generate_code.return_value = "import streamlit\nst.title('Test')"
190
+ mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
190
191
 
191
192
  # Mock validation (always errors) - Return list of errors as expected by validate_app
192
193
  mock_validator.return_value = (True, ["Persistent error"])
193
194
 
194
- result = await streamlit_handler("/notebook.ipynb")
195
+ result = await streamlit_handler("notebook.ipynb")
195
196
 
196
197
  # Verify the result indicates failure
197
198
  assert result[0] is False
198
199
  assert "Error generating streamlit code by agent" in result[2]
199
200
 
200
201
  # Verify that error correction was called 5 times (max retries)
201
- assert mock_generator.correct_error_in_generation.call_count == 5
202
+ assert mock_correct_error.call_count == 5
202
203
 
203
204
  @pytest.mark.asyncio
204
205
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
205
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
206
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
206
207
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
207
208
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
208
209
  @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):
210
+ async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
210
211
  """Test streamlit handler when file creation fails"""
211
212
  # Mock notebook parsing
212
- mock_notebook_data: dict = {"cells": []}
213
+ mock_notebook_data: List[dict] = [{"cells": []}]
213
214
  mock_parse.return_value = mock_notebook_data
214
215
 
215
216
  # Mock code generation
216
- mock_generator = AsyncMock()
217
- mock_generator.generate_streamlit_code.return_value = "import streamlit\nst.title('Test')"
218
- mock_generator_class.return_value = mock_generator
217
+ mock_generate_code.return_value = "import streamlit\nst.title('Test')"
219
218
 
220
219
  # Mock validation (no errors)
221
220
  mock_validator.return_value = (False, "")
@@ -226,7 +225,7 @@ class TestStreamlitHandler:
226
225
  # Mock clean directory check (no-op)
227
226
  mock_clean_directory.return_value = None
228
227
 
229
- result = await streamlit_handler("/path/to/notebook.ipynb")
228
+ result = await streamlit_handler("notebook.ipynb")
230
229
 
231
230
  assert result[0] is False
232
231
  assert "Permission denied" in result[2]
@@ -242,112 +241,26 @@ class TestStreamlitHandler:
242
241
  mock_parse.side_effect = FileNotFoundError("Notebook not found")
243
242
 
244
243
  with pytest.raises(FileNotFoundError, match="Notebook not found"):
245
- await streamlit_handler("/path/to/notebook.ipynb")
244
+ await streamlit_handler("notebook.ipynb")
246
245
 
247
246
  @pytest.mark.asyncio
248
247
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
249
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.StreamlitCodeGeneration')
248
+ @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
250
249
  @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):
250
+ async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generate_code, mock_parse):
252
251
  """Test streamlit handler when code generation fails"""
253
252
  # Mock notebook parsing
254
- mock_notebook_data: dict = {"cells": []}
253
+ mock_notebook_data: List[dict] = [{"cells": []}]
255
254
  mock_parse.return_value = mock_notebook_data
256
255
 
257
256
  # Mock code generation failure
258
- mock_generator = AsyncMock()
259
- mock_generator.generate_streamlit_code.side_effect = Exception("Generation failed")
260
- mock_generator_class.return_value = mock_generator
257
+ mock_generate_code.side_effect = Exception("Generation failed")
261
258
 
262
259
  # Mock clean directory check (no-op)
263
260
  mock_clean_directory.return_value = None
264
261
 
265
262
  with pytest.raises(Exception, match="Generation failed"):
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()
263
+ await streamlit_handler("notebook.ipynb")
291
264
 
292
265
 
293
- class TestCleanDirectoryCheck:
294
- """Test cases for clean_directory_check function"""
295
266
 
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()
@@ -131,15 +131,15 @@ class TestParseJupyterNotebookToExtractRequiredContent:
131
131
  result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
132
132
 
133
133
  # Check that only cell_type and source are preserved
134
- assert len(result['cells']) == 2
135
- assert result['cells'][0]['cell_type'] == 'code'
136
- assert result['cells'][0]['source'] == ["import pandas as pd\n", "df = pd.DataFrame()\n"]
137
- assert 'metadata' not in result['cells'][0]
138
- assert 'execution_count' not in result['cells'][0]
134
+ assert len(result) == 2
135
+ assert result[0]['cell_type'] == 'code'
136
+ assert result[0]['source'] == ["import pandas as pd\n", "df = pd.DataFrame()\n"]
137
+ assert 'metadata' not in result[0]
138
+ assert 'execution_count' not in result[0]
139
139
 
140
- assert result['cells'][1]['cell_type'] == 'markdown'
141
- assert result['cells'][1]['source'] == ["# Title\n", "Some text\n"]
142
- assert 'metadata' not in result['cells'][1]
140
+ assert result[1]['cell_type'] == 'markdown'
141
+ assert result[1]['source'] == ["# Title\n", "Some text\n"]
142
+ assert 'metadata' not in result[1]
143
143
 
144
144
  def test_parse_notebook_file_not_found(self):
145
145
  """Test parsing non-existent notebook file"""
@@ -171,15 +171,15 @@ class TestParseJupyterNotebookToExtractRequiredContent:
171
171
 
172
172
  result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
173
173
 
174
- assert len(result['cells']) == 3
175
- assert result['cells'][0]['cell_type'] == 'code'
176
- assert result['cells'][0]['source'] == [] # Default empty list
174
+ assert len(result) == 3
175
+ assert result[0]['cell_type'] == 'code'
176
+ assert result[0]['source'] == [] # Default empty list
177
177
 
178
- assert result['cells'][1]['cell_type'] == '' # Default empty string
179
- assert result['cells'][1]['source'] == ["some text"]
178
+ assert result[1]['cell_type'] == '' # Default empty string
179
+ assert result[1]['source'] == ["some text"]
180
180
 
181
- assert result['cells'][2]['cell_type'] == 'markdown'
182
- assert result['cells'][2]['source'] == ["# Title"]
181
+ assert result[2]['cell_type'] == 'markdown'
182
+ assert result[2]['source'] == ["# Title"]
183
183
 
184
184
  def test_parse_empty_notebook(self, tmp_path):
185
185
  """Test parsing notebook with empty cells list"""
@@ -193,4 +193,4 @@ class TestParseJupyterNotebookToExtractRequiredContent:
193
193
 
194
194
  result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
195
195
 
196
- assert result['cells'] == []
196
+ assert result == []
@@ -5,14 +5,16 @@ import os
5
5
  import tempfile
6
6
  from unittest.mock import patch, MagicMock
7
7
  from mito_ai.streamlit_conversion.validate_streamlit_app import (
8
- StreamlitValidator,
8
+ get_syntax_error,
9
+ get_runtime_errors,
10
+ check_for_errors,
9
11
  validate_app
10
12
  )
11
13
  import pytest
12
14
 
13
15
 
14
- class TestStreamlitValidator:
15
- """Test cases for StreamlitValidator class"""
16
+ class TestGetSyntaxError:
17
+ """Test cases for get_syntax_error function"""
16
18
 
17
19
  @pytest.mark.parametrize("code,expected_error,test_description", [
18
20
  # Valid Python code should return no error
@@ -34,11 +36,9 @@ class TestStreamlitValidator:
34
36
  "empty code"
35
37
  ),
36
38
  ])
37
- def test_validate_syntax(self, code, expected_error, test_description):
39
+ def test_get_syntax_error(self, code, expected_error, test_description):
38
40
  """Test syntax validation with various code inputs"""
39
- validator = StreamlitValidator()
40
-
41
- error = validator.get_syntax_error(code)
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}"
@@ -46,6 +46,9 @@ class TestStreamlitValidator:
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
+ class TestGetRuntimeErrors:
50
+ """Test cases for get_runtime_errors function"""
51
+
49
52
  @pytest.mark.parametrize("app_code,expected_error", [
50
53
  ("x = 5", None),
51
54
  ("1/0", "division by zero"),
@@ -53,9 +56,7 @@ class TestStreamlitValidator:
53
56
  ])
54
57
  def test_get_runtime_errors(self, app_code, expected_error):
55
58
  """Test getting runtime errors"""
56
- validator = StreamlitValidator()
57
-
58
- errors = validator.get_runtime_errors(app_code, '/app.py')
59
+ errors = get_runtime_errors(app_code, '/app.py')
59
60
 
60
61
  if expected_error is None:
61
62
  assert errors is None
@@ -84,24 +85,11 @@ df=pd.read_csv('data.csv')
84
85
  with open(csv_path, "w") as f:
85
86
  f.write("name,age\nJohn,25\nJane,30")
86
87
 
87
- validator = StreamlitValidator()
88
- errors = validator.get_runtime_errors(app_code, app_path)
88
+ errors = get_runtime_errors(app_code, app_path)
89
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
90
 
91
+ class TestValidateApp:
92
+ """Test cases for validate_app function"""
105
93
 
106
94
  @pytest.mark.parametrize("app_code,expected_has_validation_error,expected_error_message", [
107
95
  ("x=5", False, ""),
@@ -109,8 +97,8 @@ df=pd.read_csv('data.csv')
109
97
  ("print('Hello World'", True, "SyntaxError"),
110
98
  ("", False, ""),
111
99
  ])
112
- def test_streamlit_code_validator(self, app_code, expected_has_validation_error, expected_error_message):
113
-
100
+ def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
101
+ """Test the validate_app function"""
114
102
  has_validation_error, errors = validate_app(app_code, '/app.py')
115
103
 
116
104
  assert has_validation_error == expected_has_validation_error
@@ -70,7 +70,7 @@ class TestEnsureAppExists:
70
70
  if streamlit_handler_return is not None:
71
71
  mock_streamlit_handler.return_value = streamlit_handler_return
72
72
 
73
- success, error_msg = await ensure_app_exists(notebook_path)
73
+ success, error_msg = await ensure_app_exists(notebook_path, False, "")
74
74
 
75
75
  # Assertions
76
76
  assert success == expected_success
@@ -81,7 +81,7 @@ class TestEnsureAppExists:
81
81
 
82
82
  # Verify streamlit_handler was called or not called as expected
83
83
  if streamlit_handler_called:
84
- mock_streamlit_handler.assert_called_once_with(notebook_path)
84
+ mock_streamlit_handler.assert_called_once_with(notebook_path, "")
85
85
  else:
86
86
  mock_streamlit_handler.assert_not_called()
87
87
 
@@ -0,0 +1,2 @@
1
+ # Copyright (c) Saga Inc.
2
+ # Distributed under the terms of the GNU Affero General Public License v3.0 License.