mito-ai 0.1.45__py3-none-any.whl → 0.1.47__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.
- mito_ai/__init__.py +10 -1
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +90 -5
- mito_ai/app_deploy/handlers.py +97 -77
- mito_ai/app_deploy/models.py +16 -12
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/handlers.py +18 -20
- mito_ai/completions/models.py +4 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -1
- mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
- mito_ai/completions/prompt_builders/utils.py +14 -0
- mito_ai/constants.py +3 -0
- mito_ai/path_utils.py +56 -0
- mito_ai/streamlit_conversion/agent_utils.py +27 -106
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +166 -53
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +2 -1
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +4 -3
- mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +1 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +93 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +103 -119
- mito_ai/streamlit_conversion/streamlit_utils.py +18 -68
- mito_ai/streamlit_conversion/validate_streamlit_app.py +78 -96
- mito_ai/streamlit_preview/handlers.py +44 -85
- mito_ai/streamlit_preview/manager.py +6 -6
- mito_ai/streamlit_preview/utils.py +19 -18
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/message_history/test_message_history_utils.py +43 -19
- mito_ai/tests/providers/test_anthropic_client.py +178 -6
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +87 -114
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +42 -45
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +20 -14
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +13 -16
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +8 -6
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/message_history_utils.py +7 -4
- mito_ai/utils/telemetry_utils.py +79 -11
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js +2126 -363
- mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js +9 -9
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.684f82575fcc2e3b350c.js.map → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js.map +1 -1
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/RECORD +81 -69
- mito_ai-0.1.45.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.0c3368195d954d2ed033.js.map +0 -1
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.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.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.45.data → mito_ai-0.1.47.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.45.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.45.dist-info → mito_ai-0.1.47.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,48 +1,51 @@
|
|
|
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
|
-
from mito_ai.
|
|
15
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
|
|
11
16
|
|
|
12
17
|
# Add this line to enable async support
|
|
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,26 +88,26 @@ 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
|
-
mock_response = """```
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
+import streamlit
|
|
106
|
-
+st.title('Fixed')
|
|
105
|
+
mock_response = """```search_replace
|
|
106
|
+
>>>>>>> SEARCH
|
|
107
|
+
st.title('Test')
|
|
108
|
+
=======
|
|
109
|
+
st.title('Fixed')
|
|
110
|
+
<<<<<<< REPLACE
|
|
107
111
|
```"""
|
|
108
112
|
async def mock_async_gen():
|
|
109
113
|
for item in [mock_response]:
|
|
@@ -111,24 +115,19 @@ class TestStreamlitCodeGeneration:
|
|
|
111
115
|
|
|
112
116
|
mock_stream.return_value = mock_async_gen()
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
result = await streamlit_code_handler.correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
|
|
118
|
-
|
|
118
|
+
result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
|
|
119
|
+
|
|
119
120
|
expected_code = "import streamlit\nst.title('Fixed')\n"
|
|
120
121
|
assert result == expected_code
|
|
121
122
|
|
|
122
123
|
@pytest.mark.asyncio
|
|
123
|
-
@patch('mito_ai.streamlit_conversion.
|
|
124
|
+
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
124
125
|
async def test_correct_error_in_generation_exception(self, mock_stream):
|
|
125
126
|
"""Test exception handling in error correction"""
|
|
126
127
|
mock_stream.side_effect = Exception("API Error")
|
|
127
128
|
|
|
128
|
-
streamlit_code_handler = StreamlitCodeGeneration()
|
|
129
|
-
|
|
130
129
|
with pytest.raises(Exception, match="API Error"):
|
|
131
|
-
await
|
|
130
|
+
await correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
|
|
132
131
|
|
|
133
132
|
|
|
134
133
|
class TestStreamlitHandler:
|
|
@@ -136,134 +135,108 @@ class TestStreamlitHandler:
|
|
|
136
135
|
|
|
137
136
|
@pytest.mark.asyncio
|
|
138
137
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
139
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
138
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
140
139
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
141
140
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
142
|
-
|
|
143
|
-
async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
|
|
141
|
+
async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
144
142
|
"""Test successful streamlit handler execution"""
|
|
145
143
|
# Mock notebook parsing
|
|
146
|
-
mock_notebook_data: dict = {"cells": [{"cell_type": "code", "source": ["import pandas"]}]}
|
|
144
|
+
mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
|
|
147
145
|
mock_parse.return_value = mock_notebook_data
|
|
148
146
|
|
|
149
147
|
# 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
|
|
148
|
+
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
153
149
|
|
|
154
150
|
# Mock validation (no errors)
|
|
155
151
|
mock_validator.return_value = (False, "")
|
|
156
152
|
|
|
157
|
-
#
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# Mock clean directory check (no-op)
|
|
161
|
-
mock_clean_directory.return_value = None
|
|
162
|
-
|
|
163
|
-
result = await streamlit_handler("/path/to/notebook.ipynb")
|
|
153
|
+
# Use a relative path that will work cross-platform
|
|
154
|
+
notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
|
|
164
155
|
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
# Construct the expected app path using the same method as the production code
|
|
157
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
158
|
+
expected_app_path = get_absolute_app_path(app_directory)
|
|
159
|
+
await streamlit_handler(notebook_path)
|
|
167
160
|
|
|
168
161
|
# Verify calls
|
|
169
|
-
mock_parse.assert_called_once_with(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
mock_create_file.assert_called_once_with("/path/to", "import streamlit\nst.title('Test')")
|
|
162
|
+
mock_parse.assert_called_once_with(notebook_path)
|
|
163
|
+
mock_generate_code.assert_called_once_with(mock_notebook_data)
|
|
164
|
+
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
|
|
165
|
+
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
|
|
174
166
|
|
|
175
167
|
@pytest.mark.asyncio
|
|
176
168
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
177
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
169
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
170
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
|
|
178
171
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
179
|
-
async def test_streamlit_handler_max_retries_exceeded(self, mock_validator,
|
|
172
|
+
async def test_streamlit_handler_max_retries_exceeded(self, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
|
|
180
173
|
"""Test streamlit handler when max retries are exceeded"""
|
|
181
174
|
# Mock notebook parsing
|
|
182
|
-
mock_notebook_data: dict = {"cells": []}
|
|
175
|
+
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
183
176
|
mock_parse.return_value = mock_notebook_data
|
|
184
177
|
|
|
185
178
|
# 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
|
|
179
|
+
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
180
|
+
mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
|
|
190
181
|
|
|
191
182
|
# Mock validation (always errors) - Return list of errors as expected by validate_app
|
|
192
183
|
mock_validator.return_value = (True, ["Persistent error"])
|
|
193
184
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
assert result[0] is False
|
|
198
|
-
assert "Error generating streamlit code by agent" in result[2]
|
|
185
|
+
# Now it should raise an exception instead of returning a tuple
|
|
186
|
+
with pytest.raises(Exception):
|
|
187
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
199
188
|
|
|
200
189
|
# Verify that error correction was called 5 times (max retries)
|
|
201
|
-
assert
|
|
190
|
+
assert mock_correct_error.call_count == 5
|
|
202
191
|
|
|
203
192
|
@pytest.mark.asyncio
|
|
204
193
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
205
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
194
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
206
195
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
207
196
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
208
|
-
|
|
209
|
-
async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generator_class, mock_parse):
|
|
197
|
+
async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
210
198
|
"""Test streamlit handler when file creation fails"""
|
|
211
199
|
# Mock notebook parsing
|
|
212
|
-
mock_notebook_data: dict = {"cells": []}
|
|
200
|
+
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
213
201
|
mock_parse.return_value = mock_notebook_data
|
|
214
202
|
|
|
215
203
|
# 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
|
|
204
|
+
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
219
205
|
|
|
220
206
|
# Mock validation (no errors)
|
|
221
207
|
mock_validator.return_value = (False, "")
|
|
222
208
|
|
|
223
|
-
# Mock file creation failure
|
|
224
|
-
mock_create_file.
|
|
225
|
-
|
|
226
|
-
# Mock clean directory check (no-op)
|
|
227
|
-
mock_clean_directory.return_value = None
|
|
209
|
+
# Mock file creation failure - now it should raise an exception
|
|
210
|
+
mock_create_file.side_effect = Exception("Permission denied")
|
|
228
211
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
assert "Permission denied" in result[2]
|
|
212
|
+
# Now it should raise an exception instead of returning a tuple
|
|
213
|
+
with pytest.raises(Exception, match="Permission denied"):
|
|
214
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
233
215
|
|
|
234
216
|
@pytest.mark.asyncio
|
|
235
217
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
236
|
-
|
|
237
|
-
async def test_streamlit_handler_parse_notebook_exception(self, mock_clean_directory, mock_parse):
|
|
218
|
+
async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
|
|
238
219
|
"""Test streamlit handler when notebook parsing fails"""
|
|
239
|
-
# Mock clean directory check (no-op)
|
|
240
|
-
mock_clean_directory.return_value = None
|
|
241
220
|
|
|
242
221
|
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
243
222
|
|
|
244
223
|
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
245
|
-
await streamlit_handler("
|
|
224
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
246
225
|
|
|
247
226
|
@pytest.mark.asyncio
|
|
248
227
|
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
249
|
-
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.
|
|
250
|
-
|
|
251
|
-
async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generator_class, mock_parse):
|
|
228
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
229
|
+
async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
|
|
252
230
|
"""Test streamlit handler when code generation fails"""
|
|
253
231
|
# Mock notebook parsing
|
|
254
|
-
mock_notebook_data: dict = {"cells": []}
|
|
232
|
+
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
255
233
|
mock_parse.return_value = mock_notebook_data
|
|
256
234
|
|
|
257
235
|
# Mock code generation failure
|
|
258
|
-
|
|
259
|
-
mock_generator.generate_streamlit_code.side_effect = Exception("Generation failed")
|
|
260
|
-
mock_generator_class.return_value = mock_generator
|
|
261
|
-
|
|
262
|
-
# Mock clean directory check (no-op)
|
|
263
|
-
mock_clean_directory.return_value = None
|
|
236
|
+
mock_generate_code.side_effect = Exception("Generation failed")
|
|
264
237
|
|
|
265
238
|
with pytest.raises(Exception, match="Generation failed"):
|
|
266
|
-
await streamlit_handler("
|
|
239
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
|
|
267
240
|
|
|
268
241
|
|
|
269
242
|
|
|
@@ -11,6 +11,7 @@ from mito_ai.streamlit_conversion.streamlit_utils import (
|
|
|
11
11
|
create_app_file,
|
|
12
12
|
parse_jupyter_notebook_to_extract_required_content
|
|
13
13
|
)
|
|
14
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookDirPath, AbsoluteNotebookPath, get_absolute_notebook_path
|
|
14
15
|
from typing import Dict, Any
|
|
15
16
|
|
|
16
17
|
class TestExtractCodeBlocks:
|
|
@@ -48,55 +49,47 @@ class TestCreateAppFile:
|
|
|
48
49
|
|
|
49
50
|
def test_create_app_file_success(self, tmp_path):
|
|
50
51
|
"""Test successful file creation"""
|
|
51
|
-
|
|
52
|
+
app_path = os.path.join(str(tmp_path), "app.py")
|
|
52
53
|
code = "import streamlit\nst.title('Test')"
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
create_app_file(AbsoluteAppPath(app_path), code)
|
|
55
56
|
|
|
56
|
-
assert
|
|
57
|
-
assert
|
|
57
|
+
assert app_path is not None
|
|
58
|
+
assert os.path.exists(app_path)
|
|
58
59
|
|
|
59
60
|
# Verify file was created with correct content
|
|
60
|
-
|
|
61
|
-
assert os.path.exists(app_file_path)
|
|
62
|
-
|
|
63
|
-
with open(app_file_path, 'r') as f:
|
|
61
|
+
with open(app_path, 'r') as f:
|
|
64
62
|
content = f.read()
|
|
65
63
|
assert content == code
|
|
66
64
|
|
|
67
65
|
def test_create_app_file_io_error(self):
|
|
68
66
|
"""Test file creation with IO error"""
|
|
69
|
-
file_path = "/nonexistent/path/that/should/fail"
|
|
67
|
+
file_path = AbsoluteAppPath("/nonexistent/path/that/should/fail")
|
|
70
68
|
code = "import streamlit"
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
assert success is False
|
|
75
|
-
assert "Error creating file" in message
|
|
70
|
+
with pytest.raises(Exception):
|
|
71
|
+
create_app_file(file_path, code)
|
|
76
72
|
|
|
77
73
|
@patch('builtins.open', side_effect=Exception("Unexpected error"))
|
|
78
74
|
def test_create_app_file_unexpected_error(self, mock_open):
|
|
79
75
|
"""Test file creation with unexpected error"""
|
|
80
|
-
|
|
76
|
+
app_path = AbsoluteAppPath("/tmp/test")
|
|
81
77
|
code = "import streamlit"
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
assert success is False
|
|
86
|
-
assert "Unexpected error" in message
|
|
79
|
+
with pytest.raises(Exception, match="Unexpected error"):
|
|
80
|
+
create_app_file(app_path, code)
|
|
87
81
|
|
|
88
82
|
def test_create_app_file_empty_code(self, tmp_path):
|
|
89
83
|
"""Test creating file with empty code"""
|
|
90
|
-
|
|
84
|
+
app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
|
|
91
85
|
code = ""
|
|
92
86
|
|
|
93
|
-
|
|
87
|
+
create_app_file(app_path, code)
|
|
94
88
|
|
|
95
|
-
assert
|
|
96
|
-
assert
|
|
89
|
+
assert app_path is not None
|
|
90
|
+
assert os.path.exists(app_path)
|
|
97
91
|
|
|
98
|
-
|
|
99
|
-
with open(app_file_path, 'r') as f:
|
|
92
|
+
with open(app_path, 'r') as f:
|
|
100
93
|
content = f.read()
|
|
101
94
|
assert content == ""
|
|
102
95
|
|
|
@@ -128,23 +121,25 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
128
121
|
with open(notebook_path, 'w') as f:
|
|
129
122
|
json.dump(notebook_data, f)
|
|
130
123
|
|
|
131
|
-
|
|
124
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
125
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
132
126
|
|
|
133
127
|
# Check that only cell_type and source are preserved
|
|
134
|
-
assert len(result
|
|
135
|
-
assert result[
|
|
136
|
-
assert result[
|
|
137
|
-
assert 'metadata' not in result[
|
|
138
|
-
assert 'execution_count' not in result[
|
|
128
|
+
assert len(result) == 2
|
|
129
|
+
assert result[0]['cell_type'] == 'code'
|
|
130
|
+
assert result[0]['source'] == ["import pandas as pd\n", "df = pd.DataFrame()\n"]
|
|
131
|
+
assert 'metadata' not in result[0]
|
|
132
|
+
assert 'execution_count' not in result[0]
|
|
139
133
|
|
|
140
|
-
assert result[
|
|
141
|
-
assert result[
|
|
142
|
-
assert 'metadata' not in result[
|
|
134
|
+
assert result[1]['cell_type'] == 'markdown'
|
|
135
|
+
assert result[1]['source'] == ["# Title\n", "Some text\n"]
|
|
136
|
+
assert 'metadata' not in result[1]
|
|
143
137
|
|
|
144
138
|
def test_parse_notebook_file_not_found(self):
|
|
145
139
|
"""Test parsing non-existent notebook file"""
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
from mito_ai.utils.error_classes import StreamlitConversionError
|
|
141
|
+
with pytest.raises(StreamlitConversionError, match="Notebook file not found"):
|
|
142
|
+
parse_jupyter_notebook_to_extract_required_content(AbsoluteNotebookPath("/nonexistent/path/notebook.ipynb"))
|
|
148
143
|
|
|
149
144
|
def test_parse_notebook_with_missing_cell_fields(self, tmp_path):
|
|
150
145
|
"""Test parsing notebook where cells are missing cell_type or source"""
|
|
@@ -169,17 +164,18 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
169
164
|
with open(notebook_path, 'w') as f:
|
|
170
165
|
json.dump(notebook_data, f)
|
|
171
166
|
|
|
172
|
-
|
|
167
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
168
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
173
169
|
|
|
174
|
-
assert len(result
|
|
175
|
-
assert result[
|
|
176
|
-
assert result[
|
|
170
|
+
assert len(result) == 3
|
|
171
|
+
assert result[0]['cell_type'] == 'code'
|
|
172
|
+
assert result[0]['source'] == [] # Default empty list
|
|
177
173
|
|
|
178
|
-
assert result[
|
|
179
|
-
assert result[
|
|
174
|
+
assert result[1]['cell_type'] == '' # Default empty string
|
|
175
|
+
assert result[1]['source'] == ["some text"]
|
|
180
176
|
|
|
181
|
-
assert result[
|
|
182
|
-
assert result[
|
|
177
|
+
assert result[2]['cell_type'] == 'markdown'
|
|
178
|
+
assert result[2]['source'] == ["# Title"]
|
|
183
179
|
|
|
184
180
|
def test_parse_empty_notebook(self, tmp_path):
|
|
185
181
|
"""Test parsing notebook with empty cells list"""
|
|
@@ -191,6 +187,7 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
191
187
|
with open(notebook_path, 'w') as f:
|
|
192
188
|
json.dump(notebook_data, f)
|
|
193
189
|
|
|
194
|
-
|
|
190
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
191
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
195
192
|
|
|
196
|
-
assert result
|
|
193
|
+
assert result == []
|
|
@@ -5,14 +5,17 @@ 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
|
|
14
|
+
from mito_ai.path_utils import AbsoluteNotebookPath
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
class
|
|
15
|
-
"""Test cases for
|
|
17
|
+
class TestGetSyntaxError:
|
|
18
|
+
"""Test cases for get_syntax_error function"""
|
|
16
19
|
|
|
17
20
|
@pytest.mark.parametrize("code,expected_error,test_description", [
|
|
18
21
|
# Valid Python code should return no error
|
|
@@ -34,11 +37,9 @@ class TestStreamlitValidator:
|
|
|
34
37
|
"empty code"
|
|
35
38
|
),
|
|
36
39
|
])
|
|
37
|
-
def
|
|
40
|
+
def test_get_syntax_error(self, code, expected_error, test_description):
|
|
38
41
|
"""Test syntax validation with various code inputs"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
error = validator.get_syntax_error(code)
|
|
42
|
+
error = get_syntax_error(code)
|
|
42
43
|
|
|
43
44
|
if expected_error is None:
|
|
44
45
|
assert error is None, f"Expected no error for {test_description}"
|
|
@@ -46,6 +47,9 @@ class TestStreamlitValidator:
|
|
|
46
47
|
assert error is not None, f"Expected error for {test_description}"
|
|
47
48
|
assert expected_error in error, f"Expected '{expected_error}' in error for {test_description}"
|
|
48
49
|
|
|
50
|
+
class TestGetRuntimeErrors:
|
|
51
|
+
"""Test cases for get_runtime_errors function"""
|
|
52
|
+
|
|
49
53
|
@pytest.mark.parametrize("app_code,expected_error", [
|
|
50
54
|
("x = 5", None),
|
|
51
55
|
("1/0", "division by zero"),
|
|
@@ -53,9 +57,9 @@ class TestStreamlitValidator:
|
|
|
53
57
|
])
|
|
54
58
|
def test_get_runtime_errors(self, app_code, expected_error):
|
|
55
59
|
"""Test getting runtime errors"""
|
|
56
|
-
validator = StreamlitValidator()
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
absolute_path = AbsoluteNotebookPath('/notebook.ipynb')
|
|
62
|
+
errors = get_runtime_errors(app_code, absolute_path)
|
|
59
63
|
|
|
60
64
|
if expected_error is None:
|
|
61
65
|
assert errors is None
|
|
@@ -84,19 +88,21 @@ df=pd.read_csv('data.csv')
|
|
|
84
88
|
with open(csv_path, "w") as f:
|
|
85
89
|
f.write("name,age\nJohn,25\nJane,30")
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
errors = validator.get_runtime_errors(app_code, app_path)
|
|
91
|
+
errors = get_runtime_errors(app_code, AbsoluteNotebookPath(app_path))
|
|
89
92
|
assert errors is None
|
|
90
93
|
|
|
94
|
+
class TestValidateApp:
|
|
95
|
+
"""Test cases for validate_app function"""
|
|
96
|
+
|
|
91
97
|
@pytest.mark.parametrize("app_code,expected_has_validation_error,expected_error_message", [
|
|
92
98
|
("x=5", False, ""),
|
|
93
99
|
("1/0", True, "division by zero"),
|
|
94
100
|
("print('Hello World'", True, "SyntaxError"),
|
|
95
101
|
("", False, ""),
|
|
96
102
|
])
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
has_validation_error, errors = validate_app(app_code, '/
|
|
103
|
+
def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
|
|
104
|
+
"""Test the validate_app function"""
|
|
105
|
+
has_validation_error, errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
|
|
100
106
|
|
|
101
107
|
assert has_validation_error == expected_has_validation_error
|
|
102
108
|
assert expected_error_message in str(errors)
|