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.

Files changed (72) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/handlers.py +98 -77
  3. mito_ai/app_deploy/models.py +16 -12
  4. mito_ai/completions/models.py +5 -1
  5. mito_ai/completions/prompt_builders/agent_execution_prompt.py +10 -1
  6. mito_ai/completions/prompt_builders/agent_system_message.py +63 -4
  7. mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
  8. mito_ai/completions/prompt_builders/prompt_constants.py +1 -0
  9. mito_ai/completions/prompt_builders/utils.py +14 -0
  10. mito_ai/path_utils.py +56 -0
  11. mito_ai/streamlit_conversion/agent_utils.py +4 -201
  12. mito_ai/streamlit_conversion/prompts/prompt_constants.py +142 -152
  13. mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +3 -3
  14. mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +2 -2
  15. mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +2 -2
  16. mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +35 -46
  18. mito_ai/streamlit_conversion/streamlit_utils.py +12 -66
  19. mito_ai/streamlit_conversion/validate_streamlit_app.py +6 -21
  20. mito_ai/streamlit_preview/__init__.py +1 -2
  21. mito_ai/streamlit_preview/handlers.py +53 -85
  22. mito_ai/streamlit_preview/manager.py +7 -16
  23. mito_ai/streamlit_preview/utils.py +8 -28
  24. mito_ai/tests/message_history/test_message_history_utils.py +1 -0
  25. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
  26. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +39 -60
  27. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  28. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +25 -20
  29. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +81 -56
  30. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +24 -37
  31. mito_ai/user/handlers.py +15 -3
  32. mito_ai/utils/create.py +17 -1
  33. mito_ai/utils/error_classes.py +42 -0
  34. mito_ai/utils/message_history_utils.py +3 -1
  35. mito_ai/utils/telemetry_utils.py +78 -13
  36. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +100 -100
  37. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  38. {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
  39. 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
  40. mito_ai-0.1.48.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.5c7d84a45ddeb5704b61.js.map +1 -0
  41. 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
  42. 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
  43. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/METADATA +1 -1
  44. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/RECORD +69 -67
  45. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  46. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  47. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  48. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  49. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  50. {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
  51. {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
  52. {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
  53. {mito_ai-0.1.46.data → mito_ai-0.1.48.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  54. {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
  55. {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
  56. {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
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {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
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/WHEEL +0 -0
  71. {mito_ai-0.1.46.dist-info → mito_ai-0.1.48.dist-info}/entry_points.txt +0 -0
  72. {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
- file_path = str(tmp_path)
52
+ app_path = os.path.join(str(tmp_path), "app.py")
52
53
  code = "import streamlit\nst.title('Test')"
53
54
 
54
- success, app_path, message = create_app_file(file_path, code)
55
+ create_app_file(AbsoluteAppPath(app_path), code)
55
56
 
56
- assert success is True
57
- assert "Successfully created" in message
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
- app_file_path = os.path.join(file_path, "app.py")
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
- success, app_path, message = create_app_file(file_path, code)
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
- file_path = "/tmp/test"
76
+ app_path = AbsoluteAppPath("/tmp/test")
81
77
  code = "import streamlit"
82
78
 
83
- success, app_path, message = create_app_file(file_path, code)
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
- file_path = str(tmp_path)
84
+ app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
91
85
  code = ""
92
86
 
93
- success, app_path, message = create_app_file(file_path, code)
87
+ create_app_file(app_path, code)
94
88
 
95
- assert success is True
96
- assert "Successfully created" in message
89
+ assert app_path is not None
90
+ assert os.path.exists(app_path)
97
91
 
98
- app_file_path = os.path.join(file_path, "app.py")
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
- result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
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
- with pytest.raises(FileNotFoundError, match="Notebook file not found"):
147
- parse_jupyter_notebook_to_extract_required_content("/nonexistent/path/notebook.ipynb")
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
- result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
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
- result = parse_jupyter_notebook_to_extract_required_content(str(notebook_path))
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
- "import streamlit\nst.title('Hello World')",
23
- None,
24
- "valid Python code"
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
- "import streamlit\nst.title('Hello World'",
29
- "SyntaxError",
30
- "invalid Python code"
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
- None,
36
- "empty code"
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
- errors = get_runtime_errors(app_code, '/app.py')
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,expected_has_validation_error,expected_error_message", [
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
- ("print('Hello World'", True, "SyntaxError"),
99
+ # Syntax errors are caught during runtime
98
100
  ("", False, ""),
99
101
  ])
100
- def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
102
+ def test_validate_app(self, app_code, expected_has_errors, expected_error_message):
101
103
  """Test the validate_app function"""
102
- has_validation_error, errors = validate_app(app_code, '/app.py')
104
+ errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
103
105
 
104
- assert has_validation_error == expected_has_validation_error
105
- assert expected_error_message in str(errors)
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.utils import ensure_app_exists
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 TestEnsureAppExists:
12
- """Test cases for the ensure_app_exists function."""
12
+ class TestStreamlitPreviewHandler:
13
+ """Test cases for the StreamlitPreviewHandler."""
13
14
 
14
15
  @pytest.mark.asyncio
15
16
  @pytest.mark.parametrize(
16
- "app_exists,streamlit_handler_success,expected_success,expected_error,streamlit_handler_called,streamlit_handler_return",
17
+ "app_exists,force_recreate,streamlit_handler_called",
17
18
  [
18
- # Test case 1: App exists, should use existing file
19
- (
20
- True, # app_exists
21
- True, # streamlit_handler_success (not relevant)
22
- True, # expected_success
23
- "", # expected_error
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
- "app_exists_uses_existing_file",
39
- "app_does_not_exist_generates_new_one_success",
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 test_ensure_app_exists(
32
+ async def test_post_handler_app_generation(
43
33
  self,
44
34
  app_exists,
45
- streamlit_handler_success,
46
- expected_success,
47
- expected_error,
35
+ force_recreate,
48
36
  streamlit_handler_called,
49
- streamlit_handler_return,
50
37
  ):
51
- """Test ensure_app_exists function with various scenarios."""
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
- # Set up app_path based on whether app exists
56
- app_path = os.path.join(temp_dir, "app.py") if app_exists else None
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
- # Mock get_app_path to return the appropriate value
65
- with patch('mito_ai.streamlit_preview.utils.get_app_path') as mock_get_app_path:
66
- mock_get_app_path.return_value = app_path
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
- # Mock streamlit_handler
69
- with patch('mito_ai.streamlit_preview.utils.streamlit_handler') as mock_streamlit_handler:
70
- if streamlit_handler_return is not None:
71
- mock_streamlit_handler.return_value = streamlit_handler_return
72
-
73
- success, error_msg = await ensure_app_exists(notebook_path, False, "")
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
- success, message, port = manager.start_streamlit_preview(app_directory, preview_id)
81
+ port = manager.start_streamlit_preview(app_directory, preview_id)
85
82
 
86
83
  # Assertions
87
- assert success == expected_success
88
- if expected_success:
89
- assert "successfully" in message.lower()
90
- assert isinstance(port, int)
91
- assert port > 0
92
-
93
- # Verify subprocess was called correctly
94
- mock_popen.assert_called_once()
95
- call_args = mock_popen.call_args
96
- assert "streamlit" in call_args[0][0]
97
- assert "run" in call_args[0][0]
98
- assert "--server.headless" in call_args[0][0]
99
- assert "--server.address" in call_args[0][0]
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
- with patch('tempfile.mkdtemp', side_effect=exception_type):
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
- assert success is False
116
- assert expected_message in message.lower()
117
- assert port is None
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
- success, _, port = manager.start_streamlit_preview("/tmp/test_dir", preview_id)
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 = get_user_field(key)
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() # Log the new user
33
- self.finish(json.dumps({"status": "success", "key": key, "value": data["value"]}))
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
- # Then, we take special care to put all the testing/CI environments
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: