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.
- mito_ai/__init__.py +10 -1
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +92 -8
- mito_ai/app_deploy/app_deploy_utils.py +25 -0
- mito_ai/app_deploy/handlers.py +9 -12
- mito_ai/app_deploy/models.py +4 -1
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/handlers.py +44 -20
- mito_ai/completions/models.py +1 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +22 -4
- mito_ai/constants.py +3 -0
- mito_ai/streamlit_conversion/agent_utils.py +148 -30
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +147 -24
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +2 -2
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +101 -104
- mito_ai/streamlit_conversion/streamlit_system_prompt.py +1 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +18 -17
- mito_ai/streamlit_conversion/validate_streamlit_app.py +66 -62
- mito_ai/streamlit_preview/handlers.py +5 -3
- mito_ai/streamlit_preview/utils.py +11 -7
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +71 -0
- mito_ai/tests/message_history/test_message_history_utils.py +43 -19
- mito_ai/tests/providers/test_anthropic_client.py +180 -8
- mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +368 -0
- mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +533 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +71 -158
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +16 -16
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +16 -28
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +2 -2
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/test_anthropic_utils.py +4 -4
- mito_ai/user/handlers.py +33 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +15 -21
- mito_ai/utils/message_history_utils.py +4 -3
- mito_ai/utils/telemetry_utils.py +7 -4
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {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
- 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
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +1 -0
- 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
- 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
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/METADATA +2 -2
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/RECORD +75 -63
- mito_ai-0.1.44.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.cf2e3ad2797fbb53826b.js.map +0 -1
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {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
- {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
- {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
- {mito_ai-0.1.44.data → mito_ai-0.1.46.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.44.dist-info → mito_ai-0.1.46.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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
|
|
17
|
-
"""Test cases for
|
|
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.
|
|
21
|
-
async def
|
|
22
|
-
"""Test
|
|
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
|
-
|
|
30
|
-
|
|
34
|
+
messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
|
|
35
|
+
response = await get_response_from_agent(messages)
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
assert
|
|
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.
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
77
|
-
async def
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
187
|
-
|
|
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("
|
|
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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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("
|
|
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("
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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("
|
|
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
|
|
135
|
-
assert result[
|
|
136
|
-
assert result[
|
|
137
|
-
assert 'metadata' not in result[
|
|
138
|
-
assert 'execution_count' not in result[
|
|
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[
|
|
141
|
-
assert result[
|
|
142
|
-
assert 'metadata' not in result[
|
|
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
|
|
175
|
-
assert result[
|
|
176
|
-
assert result[
|
|
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[
|
|
179
|
-
assert result[
|
|
178
|
+
assert result[1]['cell_type'] == '' # Default empty string
|
|
179
|
+
assert result[1]['source'] == ["some text"]
|
|
180
180
|
|
|
181
|
-
assert result[
|
|
182
|
-
assert result[
|
|
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
|
|
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
|
-
|
|
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
|
|
15
|
-
"""Test cases for
|
|
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
|
|
39
|
+
def test_get_syntax_error(self, code, expected_error, test_description):
|
|
38
40
|
"""Test syntax validation with various code inputs"""
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|