mito-ai 0.1.46__py3-none-any.whl → 0.1.48__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/_version.py +1 -1
- mito_ai/app_deploy/handlers.py +98 -77
- mito_ai/app_deploy/models.py +16 -12
- mito_ai/completions/models.py +5 -1
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +10 -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/path_utils.py +56 -0
- mito_ai/streamlit_conversion/agent_utils.py +4 -201
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
- mito_ai/streamlit_conversion/streamlit_utils.py +12 -66
- mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
- mito_ai/streamlit_preview/__init__.py +1 -2
- mito_ai/streamlit_preview/handlers.py +53 -85
- mito_ai/streamlit_preview/manager.py +7 -16
- mito_ai/streamlit_preview/utils.py +8 -28
- mito_ai/tests/message_history/test_message_history_utils.py +1 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +39 -60
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +81 -56
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +24 -37
- mito_ai/user/handlers.py +15 -3
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/message_history_utils.py +3 -1
- mito_ai/utils/telemetry_utils.py +78 -13
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js +1515 -449
- mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js.map +1 -0
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js +18 -18
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.045d65d1de6fde3f3b72.js.map +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/METADATA +1 -1
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/RECORD +69 -67
- mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
- mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
- mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
- /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.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.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.46.data → mito_ai-0.1.48.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.46.dist-info → mito_ai-0.1.48.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,7 +121,8 @@ 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
128
|
assert len(result) == 2
|
|
@@ -143,8 +137,9 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
143
137
|
|
|
144
138
|
def test_parse_notebook_file_not_found(self):
|
|
145
139
|
"""Test parsing non-existent notebook file"""
|
|
146
|
-
|
|
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,7 +164,8 @@ 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
170
|
assert len(result) == 3
|
|
175
171
|
assert result[0]['cell_type'] == 'code'
|
|
@@ -191,6 +187,7 @@ class TestParseJupyterNotebookToExtractRequiredContent:
|
|
|
191
187
|
with open(notebook_path, 'w') as f:
|
|
192
188
|
json.dump(notebook_data, f)
|
|
193
189
|
|
|
194
|
-
|
|
190
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
191
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
195
192
|
|
|
196
193
|
assert result == []
|
|
@@ -7,10 +7,10 @@ from unittest.mock import patch, MagicMock
|
|
|
7
7
|
from mito_ai.streamlit_conversion.validate_streamlit_app import (
|
|
8
8
|
get_syntax_error,
|
|
9
9
|
get_runtime_errors,
|
|
10
|
-
check_for_errors,
|
|
11
10
|
validate_app
|
|
12
11
|
)
|
|
13
12
|
import pytest
|
|
13
|
+
from mito_ai.path_utils import AbsoluteNotebookPath
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class TestGetSyntaxError:
|
|
@@ -19,33 +19,33 @@ class TestGetSyntaxError:
|
|
|
19
19
|
@pytest.mark.parametrize("code,expected_error,test_description", [
|
|
20
20
|
# Valid Python code should return no error
|
|
21
21
|
(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
"import streamlit\nst.title('Hello World')",
|
|
23
|
+
None,
|
|
24
|
+
"valid Python code"
|
|
25
25
|
),
|
|
26
26
|
# Invalid Python syntax should be caught
|
|
27
27
|
(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
"import streamlit\nst.title('Hello World'",
|
|
29
|
+
"SyntaxError",
|
|
30
|
+
"invalid Python code"
|
|
31
31
|
),
|
|
32
32
|
# Empty streamlit app is valid
|
|
33
33
|
(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
"",
|
|
35
|
+
None,
|
|
36
|
+
"empty code"
|
|
37
37
|
),
|
|
38
38
|
])
|
|
39
39
|
def test_get_syntax_error(self, code, expected_error, test_description):
|
|
40
40
|
"""Test syntax validation with various code inputs"""
|
|
41
41
|
error = get_syntax_error(code)
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
if expected_error is None:
|
|
44
44
|
assert error is None, f"Expected no error for {test_description}"
|
|
45
45
|
else:
|
|
46
46
|
assert error is not None, f"Expected error for {test_description}"
|
|
47
47
|
assert expected_error in error, f"Expected '{expected_error}' in error for {test_description}"
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
class TestGetRuntimeErrors:
|
|
50
50
|
"""Test cases for get_runtime_errors function"""
|
|
51
51
|
|
|
@@ -56,7 +56,9 @@ class TestGetRuntimeErrors:
|
|
|
56
56
|
])
|
|
57
57
|
def test_get_runtime_errors(self, app_code, expected_error):
|
|
58
58
|
"""Test getting runtime errors"""
|
|
59
|
-
|
|
59
|
+
|
|
60
|
+
absolute_path = AbsoluteNotebookPath('/notebook.ipynb')
|
|
61
|
+
errors = get_runtime_errors(app_code, absolute_path)
|
|
60
62
|
|
|
61
63
|
if expected_error is None:
|
|
62
64
|
assert errors is None
|
|
@@ -85,23 +87,26 @@ df=pd.read_csv('data.csv')
|
|
|
85
87
|
with open(csv_path, "w") as f:
|
|
86
88
|
f.write("name,age\nJohn,25\nJane,30")
|
|
87
89
|
|
|
88
|
-
errors = get_runtime_errors(app_code, app_path)
|
|
90
|
+
errors = get_runtime_errors(app_code, AbsoluteNotebookPath(app_path))
|
|
89
91
|
assert errors is None
|
|
90
92
|
|
|
91
93
|
class TestValidateApp:
|
|
92
94
|
"""Test cases for validate_app function"""
|
|
93
95
|
|
|
94
|
-
@pytest.mark.parametrize("app_code,
|
|
96
|
+
@pytest.mark.parametrize("app_code,expected_has_errors,expected_error_message", [
|
|
95
97
|
("x=5", False, ""),
|
|
96
98
|
("1/0", True, "division by zero"),
|
|
97
|
-
|
|
99
|
+
# Syntax errors are caught during runtime
|
|
98
100
|
("", False, ""),
|
|
99
101
|
])
|
|
100
|
-
def test_validate_app(self, app_code,
|
|
102
|
+
def test_validate_app(self, app_code, expected_has_errors, expected_error_message):
|
|
101
103
|
"""Test the validate_app function"""
|
|
102
|
-
|
|
104
|
+
errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
assert
|
|
106
|
+
has_errors = len(errors) > 0
|
|
107
|
+
assert has_errors == expected_has_errors
|
|
108
|
+
if expected_error_message:
|
|
109
|
+
errors_str = str(errors)
|
|
110
|
+
assert expected_error_message in errors_str
|
|
106
111
|
|
|
107
112
|
|
|
@@ -4,85 +4,110 @@
|
|
|
4
4
|
import pytest
|
|
5
5
|
import os
|
|
6
6
|
import tempfile
|
|
7
|
-
from unittest.mock import patch, AsyncMock, MagicMock
|
|
8
|
-
from mito_ai.streamlit_preview.
|
|
7
|
+
from unittest.mock import patch, Mock, AsyncMock, MagicMock
|
|
8
|
+
from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
|
|
9
|
+
from mito_ai.path_utils import AbsoluteNotebookPath
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
class
|
|
12
|
-
"""Test cases for the
|
|
12
|
+
class TestStreamlitPreviewHandler:
|
|
13
|
+
"""Test cases for the StreamlitPreviewHandler."""
|
|
13
14
|
|
|
14
15
|
@pytest.mark.asyncio
|
|
15
16
|
@pytest.mark.parametrize(
|
|
16
|
-
"app_exists,
|
|
17
|
+
"app_exists,force_recreate,streamlit_handler_called",
|
|
17
18
|
[
|
|
18
|
-
# Test case 1: App exists, should
|
|
19
|
-
(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
False, # streamlit_handler_called
|
|
25
|
-
None, # streamlit_handler_return (not used)
|
|
26
|
-
),
|
|
27
|
-
# Test case 2: App doesn't exist, streamlit_handler succeeds
|
|
28
|
-
(
|
|
29
|
-
False, # app_exists
|
|
30
|
-
True, # streamlit_handler_success
|
|
31
|
-
True, # expected_success
|
|
32
|
-
"", # expected_error
|
|
33
|
-
True, # streamlit_handler_called
|
|
34
|
-
(True, "/path/to/app.py", "Success"), # streamlit_handler_return
|
|
35
|
-
)
|
|
19
|
+
# Test case 1: App exists, not forcing recreate - should not call streamlit_handler
|
|
20
|
+
(True, False, False),
|
|
21
|
+
# Test case 2: App doesn't exist - should call streamlit_handler
|
|
22
|
+
(False, False, True),
|
|
23
|
+
# Test case 3: App exists but forcing recreate - should call streamlit_handler
|
|
24
|
+
(True, True, True),
|
|
36
25
|
],
|
|
37
26
|
ids=[
|
|
38
|
-
"
|
|
39
|
-
"
|
|
27
|
+
"app_exists_no_force_recreate",
|
|
28
|
+
"app_does_not_exist_generates_new_one",
|
|
29
|
+
"app_exists_force_recreate",
|
|
40
30
|
]
|
|
41
31
|
)
|
|
42
|
-
async def
|
|
32
|
+
async def test_post_handler_app_generation(
|
|
43
33
|
self,
|
|
44
34
|
app_exists,
|
|
45
|
-
|
|
46
|
-
expected_success,
|
|
47
|
-
expected_error,
|
|
35
|
+
force_recreate,
|
|
48
36
|
streamlit_handler_called,
|
|
49
|
-
streamlit_handler_return,
|
|
50
37
|
):
|
|
51
|
-
"""Test
|
|
38
|
+
"""Test StreamlitPreviewHandler POST method with various scenarios."""
|
|
52
39
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
53
40
|
notebook_path = os.path.join(temp_dir, "test_notebook.ipynb")
|
|
41
|
+
app_path = os.path.join(temp_dir, "app.py")
|
|
54
42
|
|
|
55
|
-
#
|
|
56
|
-
|
|
43
|
+
# Create notebook file
|
|
44
|
+
with open(notebook_path, "w") as f:
|
|
45
|
+
f.write('{"cells": []}')
|
|
57
46
|
|
|
58
47
|
# Create app.py file if it should exist
|
|
59
48
|
if app_exists:
|
|
60
|
-
assert app_path is not None # Type assertion for mypy
|
|
61
49
|
with open(app_path, "w") as f:
|
|
62
50
|
f.write("import streamlit as st\nst.write('Hello World')")
|
|
63
51
|
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
52
|
+
# Create a properly mocked Tornado application with required attributes
|
|
53
|
+
mock_application = MagicMock()
|
|
54
|
+
mock_application.ui_methods = {}
|
|
55
|
+
mock_application.ui_modules = {}
|
|
56
|
+
mock_application.settings = {}
|
|
57
|
+
|
|
58
|
+
# Create a mock request with necessary tornado setup
|
|
59
|
+
mock_request = MagicMock()
|
|
60
|
+
mock_request.connection = MagicMock()
|
|
61
|
+
mock_request.connection.context = MagicMock()
|
|
62
|
+
|
|
63
|
+
# Create handler instance
|
|
64
|
+
handler = StreamlitPreviewHandler(
|
|
65
|
+
application=mock_application,
|
|
66
|
+
request=mock_request,
|
|
67
|
+
)
|
|
68
|
+
handler.initialize()
|
|
69
|
+
|
|
70
|
+
# Mock authentication - set current_user to bypass @tornado.web.authenticated
|
|
71
|
+
handler.current_user = "test_user" # type: ignore
|
|
72
|
+
|
|
73
|
+
# Mock the finish method and other handler methods
|
|
74
|
+
finish_called = []
|
|
75
|
+
def mock_finish_func(response):
|
|
76
|
+
finish_called.append(response)
|
|
77
|
+
|
|
78
|
+
# Mock streamlit_handler and preview manager
|
|
79
|
+
with patch.object(handler, 'get_json_body', return_value={
|
|
80
|
+
"notebook_path": notebook_path,
|
|
81
|
+
"force_recreate": force_recreate,
|
|
82
|
+
"edit_prompt": ""
|
|
83
|
+
}), \
|
|
84
|
+
patch.object(handler, 'finish', side_effect=mock_finish_func), \
|
|
85
|
+
patch.object(handler, 'set_status'), \
|
|
86
|
+
patch('mito_ai.streamlit_preview.handlers.streamlit_handler', new_callable=AsyncMock) as mock_streamlit_handler, \
|
|
87
|
+
patch.object(handler.preview_manager, 'start_streamlit_preview', return_value=8501) as mock_start_preview, \
|
|
88
|
+
patch('mito_ai.streamlit_preview.handlers.log_streamlit_app_preview_success'):
|
|
89
|
+
|
|
90
|
+
# Call the handler
|
|
91
|
+
await handler.post() # type: ignore[misc]
|
|
92
|
+
|
|
93
|
+
# Verify streamlit_handler was called or not called as expected
|
|
94
|
+
if streamlit_handler_called:
|
|
95
|
+
assert mock_streamlit_handler.called
|
|
96
|
+
# Verify it was called with the absolute notebook path
|
|
97
|
+
call_args = mock_streamlit_handler.call_args
|
|
98
|
+
assert call_args[0][0] == notebook_path # First argument should be the notebook path
|
|
99
|
+
assert call_args[0][1] == "" # Second argument should be the edit_prompt
|
|
100
|
+
else:
|
|
101
|
+
mock_streamlit_handler.assert_not_called()
|
|
102
|
+
|
|
103
|
+
# Verify preview was started
|
|
104
|
+
mock_start_preview.assert_called_once()
|
|
67
105
|
|
|
68
|
-
#
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Assertions
|
|
76
|
-
assert success == expected_success
|
|
77
|
-
assert error_msg == expected_error
|
|
78
|
-
|
|
79
|
-
# Verify get_app_path was called with the correct directory
|
|
80
|
-
mock_get_app_path.assert_called_once_with(temp_dir)
|
|
81
|
-
|
|
82
|
-
# Verify streamlit_handler was called or not called as expected
|
|
83
|
-
if streamlit_handler_called:
|
|
84
|
-
mock_streamlit_handler.assert_called_once_with(notebook_path, "")
|
|
85
|
-
else:
|
|
86
|
-
mock_streamlit_handler.assert_not_called()
|
|
106
|
+
# Verify response was sent
|
|
107
|
+
assert len(finish_called) == 1
|
|
108
|
+
response = finish_called[0]
|
|
109
|
+
assert response["type"] == "success"
|
|
110
|
+
assert "port" in response
|
|
111
|
+
assert "id" in response
|
|
87
112
|
|
|
88
113
|
|
|
@@ -15,11 +15,8 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
from mito_ai.streamlit_preview.manager import (
|
|
17
17
|
StreamlitPreviewManager,
|
|
18
|
-
PreviewProcess
|
|
19
|
-
get_preview_manager
|
|
18
|
+
PreviewProcess
|
|
20
19
|
)
|
|
21
|
-
from mito_ai.streamlit_preview.handlers import StreamlitPreviewHandler
|
|
22
|
-
from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
class TestStreamlitPreviewManager:
|
|
@@ -81,25 +78,22 @@ st.write("Hello, World!")
|
|
|
81
78
|
mock_requests_get.return_value = mock_response
|
|
82
79
|
|
|
83
80
|
# Test
|
|
84
|
-
|
|
81
|
+
port = manager.start_streamlit_preview(app_directory, preview_id)
|
|
85
82
|
|
|
86
83
|
# Assertions
|
|
87
|
-
assert
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Cleanup
|
|
102
|
-
manager.stop_preview(preview_id)
|
|
84
|
+
assert isinstance(port, int)
|
|
85
|
+
assert port > 0
|
|
86
|
+
|
|
87
|
+
# Verify subprocess was called correctly
|
|
88
|
+
mock_popen.assert_called_once()
|
|
89
|
+
call_args = mock_popen.call_args
|
|
90
|
+
assert "streamlit" in call_args[0][0]
|
|
91
|
+
assert "run" in call_args[0][0]
|
|
92
|
+
assert "--server.headless" in call_args[0][0]
|
|
93
|
+
assert "--server.address" in call_args[0][0]
|
|
94
|
+
|
|
95
|
+
# Cleanup
|
|
96
|
+
manager.stop_preview(preview_id)
|
|
103
97
|
|
|
104
98
|
@pytest.mark.parametrize("exception_type,expected_message", [
|
|
105
99
|
(Exception("Temp dir creation failed"), "failed to start preview"),
|
|
@@ -108,13 +102,15 @@ st.write("Hello, World!")
|
|
|
108
102
|
])
|
|
109
103
|
def test_start_streamlit_preview_exceptions(self, manager, sample_app_code, exception_type, expected_message):
|
|
110
104
|
"""Test streamlit preview start with different exceptions."""
|
|
111
|
-
|
|
105
|
+
from mito_ai.utils.error_classes import StreamlitPreviewError
|
|
106
|
+
|
|
107
|
+
with patch('subprocess.Popen', side_effect=exception_type):
|
|
112
108
|
app_directory = "/tmp/test_dir"
|
|
113
|
-
success, message, port = manager.start_streamlit_preview(app_directory, "test_preview")
|
|
114
109
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
with pytest.raises(StreamlitPreviewError) as exc_info:
|
|
111
|
+
manager.start_streamlit_preview(app_directory, "test_preview")
|
|
112
|
+
|
|
113
|
+
assert expected_message in str(exc_info.value).lower()
|
|
118
114
|
|
|
119
115
|
@pytest.mark.parametrize("preview_id,expected_result", [
|
|
120
116
|
("existing_preview", True),
|
|
@@ -245,15 +241,7 @@ st.write("Hello, World!")
|
|
|
245
241
|
|
|
246
242
|
assert preview.proc == proc
|
|
247
243
|
assert preview.port == port
|
|
248
|
-
|
|
249
|
-
def test_get_preview_manager_singleton(self):
|
|
250
|
-
"""Test that get_preview_manager returns the same instance."""
|
|
251
|
-
manager1 = get_preview_manager()
|
|
252
|
-
manager2 = get_preview_manager()
|
|
253
|
-
|
|
254
|
-
assert manager1 is manager2
|
|
255
|
-
assert isinstance(manager1, StreamlitPreviewManager)
|
|
256
|
-
|
|
244
|
+
|
|
257
245
|
@pytest.mark.parametrize("num_previews", [1, 2, 3])
|
|
258
246
|
def test_concurrent_previews(self, manager, sample_app_code, num_previews):
|
|
259
247
|
"""Test managing multiple concurrent previews."""
|
|
@@ -284,8 +272,7 @@ st.write("Hello, World!")
|
|
|
284
272
|
|
|
285
273
|
# Start multiple previews
|
|
286
274
|
for preview_id in preview_ids:
|
|
287
|
-
|
|
288
|
-
assert success is True
|
|
275
|
+
port = manager.start_streamlit_preview("/tmp/test_dir", preview_id)
|
|
289
276
|
ports.append(port)
|
|
290
277
|
|
|
291
278
|
# Assertions
|
mito_ai/user/handlers.py
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import tornado
|
|
6
|
+
from typing import Any, Optional
|
|
6
7
|
from jupyter_server.base.handlers import APIHandler
|
|
7
8
|
from mito_ai.utils.db import get_user_field, set_user_field
|
|
8
9
|
from mito_ai.utils.telemetry_utils import identify
|
|
10
|
+
from mito_ai.utils.version_utils import is_pro
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class UserHandler(APIHandler):
|
|
@@ -13,7 +15,15 @@ class UserHandler(APIHandler):
|
|
|
13
15
|
|
|
14
16
|
@tornado.web.authenticated
|
|
15
17
|
def get(self, key: str) -> None:
|
|
16
|
-
value =
|
|
18
|
+
value: Optional[Any] = None
|
|
19
|
+
|
|
20
|
+
if key == "is_pro":
|
|
21
|
+
# Special case, since we don't store this key
|
|
22
|
+
# in the user.json file.
|
|
23
|
+
value = str(is_pro())
|
|
24
|
+
else:
|
|
25
|
+
value = get_user_field(key)
|
|
26
|
+
|
|
17
27
|
if value is None:
|
|
18
28
|
self.set_status(404)
|
|
19
29
|
self.finish(json.dumps({"error": f"User field with key '{key}' not found"}))
|
|
@@ -29,5 +39,7 @@ class UserHandler(APIHandler):
|
|
|
29
39
|
return
|
|
30
40
|
|
|
31
41
|
set_user_field(key, data["value"])
|
|
32
|
-
identify()
|
|
33
|
-
self.finish(
|
|
42
|
+
identify() # Log the new user
|
|
43
|
+
self.finish(
|
|
44
|
+
json.dumps({"status": "success", "key": key, "value": data["value"]})
|
|
45
|
+
)
|
mito_ai/utils/create.py
CHANGED
|
@@ -36,6 +36,17 @@ def is_user_json_exists_and_valid_json() -> bool:
|
|
|
36
36
|
except:
|
|
37
37
|
return False
|
|
38
38
|
|
|
39
|
+
def get_temp_user_id() -> Optional[str]:
|
|
40
|
+
"""
|
|
41
|
+
Looks for a temporary user ID, generated by the desktop app.
|
|
42
|
+
"""
|
|
43
|
+
temp_user_id_path = os.path.join(MITO_FOLDER, 'temp_user_id.txt')
|
|
44
|
+
|
|
45
|
+
if os.path.exists(temp_user_id_path):
|
|
46
|
+
with open(temp_user_id_path, 'r') as f:
|
|
47
|
+
return f.read()
|
|
48
|
+
|
|
49
|
+
return None
|
|
39
50
|
|
|
40
51
|
def try_create_user_json_file() -> None:
|
|
41
52
|
|
|
@@ -50,7 +61,12 @@ def try_create_user_json_file() -> None:
|
|
|
50
61
|
with open(USER_JSON_PATH, 'w+') as f:
|
|
51
62
|
f.write(json.dumps(USER_JSON_DEFAULT))
|
|
52
63
|
|
|
53
|
-
#
|
|
64
|
+
# Next, look for a temp user id
|
|
65
|
+
temp_user_id = get_temp_user_id()
|
|
66
|
+
if temp_user_id:
|
|
67
|
+
set_user_field(UJ_STATIC_USER_ID, temp_user_id)
|
|
68
|
+
|
|
69
|
+
# Finally, we take special care to put all the testing/CI environments
|
|
54
70
|
# (e.g. Github actions) under one ID and email
|
|
55
71
|
if is_running_test():
|
|
56
72
|
set_user_field(UJ_STATIC_USER_ID, GITHUB_ACTION_ID)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from mito_ai.app_deploy.models import AppDeployError
|
|
5
|
+
|
|
6
|
+
class MitoAppError(Exception):
|
|
7
|
+
"""Exception raised for custom error in the application."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, error_code: int) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.error_code = error_code
|
|
13
|
+
|
|
14
|
+
class StreamlitPreviewError(MitoAppError):
|
|
15
|
+
def __str__(self) -> str:
|
|
16
|
+
return f"[PreviewError]: {self.message} (Error Code: {self.error_code})"
|
|
17
|
+
|
|
18
|
+
class StreamlitConversionError(MitoAppError):
|
|
19
|
+
def __str__(self) -> str:
|
|
20
|
+
return f"[ConversionError]: {self.message} (Error Code: {self.error_code})"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class StreamlitDeploymentError(MitoAppError):
|
|
24
|
+
"""Raised when a deployment operation fails."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, error: AppDeployError):
|
|
27
|
+
self.error = error
|
|
28
|
+
self.error_type = error.error_type
|
|
29
|
+
self.message_id = getattr(error, "message_id", "ErrorMessageID")
|
|
30
|
+
self.error_code = getattr(error, "error_code", 500)
|
|
31
|
+
self.hint = getattr(error, "hint", "")
|
|
32
|
+
self.traceback = getattr(error, "traceback", "")
|
|
33
|
+
self.error_type = getattr(error, "error_type", "Error")
|
|
34
|
+
self.message = error.message
|
|
35
|
+
print(f"self_message: {self.message}")
|
|
36
|
+
super().__init__(self.message, self.error_code)
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
base = f"[DeploymentError]: {self.message} (Error Code: {self.error_code})"
|
|
40
|
+
if self.hint:
|
|
41
|
+
base += f"\nHint: {self.hint}"
|
|
42
|
+
return base
|
|
@@ -10,6 +10,7 @@ from mito_ai.completions.prompt_builders.prompt_constants import (
|
|
|
10
10
|
ACTIVE_CELL_OUTPUT_SECTION_HEADING,
|
|
11
11
|
GET_CELL_OUTPUT_TOOL_RESPONSE_SECTION_HEADING,
|
|
12
12
|
FILES_SECTION_HEADING,
|
|
13
|
+
STREAMLIT_APP_STATUS_SECTION_HEADING,
|
|
13
14
|
VARIABLES_SECTION_HEADING,
|
|
14
15
|
JUPYTER_NOTEBOOK_SECTION_HEADING,
|
|
15
16
|
CONTENT_REMOVED_PLACEHOLDER
|
|
@@ -31,7 +32,8 @@ def trim_sections_from_message_content(content: str) -> str:
|
|
|
31
32
|
JUPYTER_NOTEBOOK_SECTION_HEADING,
|
|
32
33
|
GET_CELL_OUTPUT_TOOL_RESPONSE_SECTION_HEADING,
|
|
33
34
|
ACTIVE_CELL_OUTPUT_SECTION_HEADING,
|
|
34
|
-
ACTIVE_CELL_ID_SECTION_HEADING
|
|
35
|
+
ACTIVE_CELL_ID_SECTION_HEADING,
|
|
36
|
+
STREAMLIT_APP_STATUS_SECTION_HEADING
|
|
35
37
|
]
|
|
36
38
|
|
|
37
39
|
for heading in section_headings:
|