mito-ai 0.1.46__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.

Files changed (70) hide show
  1. mito_ai/_version.py +1 -1
  2. mito_ai/app_deploy/handlers.py +97 -77
  3. mito_ai/app_deploy/models.py +16 -12
  4. mito_ai/completions/models.py +4 -1
  5. mito_ai/completions/prompt_builders/agent_execution_prompt.py +6 -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 +93 -0
  17. mito_ai/streamlit_conversion/streamlit_agent_handler.py +29 -39
  18. mito_ai/streamlit_conversion/streamlit_utils.py +11 -64
  19. mito_ai/streamlit_conversion/validate_streamlit_app.py +5 -18
  20. mito_ai/streamlit_preview/handlers.py +44 -84
  21. mito_ai/streamlit_preview/manager.py +6 -6
  22. mito_ai/streamlit_preview/utils.py +16 -19
  23. mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +226 -0
  24. mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +29 -53
  25. mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +26 -29
  26. mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +6 -3
  27. mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +12 -15
  28. mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +22 -26
  29. mito_ai/user/handlers.py +15 -3
  30. mito_ai/utils/create.py +17 -1
  31. mito_ai/utils/error_classes.py +42 -0
  32. mito_ai/utils/message_history_utils.py +3 -1
  33. mito_ai/utils/telemetry_utils.py +75 -10
  34. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +1 -1
  35. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/package.json +2 -2
  36. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +1 -1
  37. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js +1274 -293
  38. mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.2db61d2b629817845901.js.map +1 -0
  39. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js +7 -7
  40. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.54126ab6511271265443.js.map → mito_ai-0.1.47.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.e22c6cd4e56c32116daa.js.map +1 -1
  41. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/METADATA +1 -1
  42. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/RECORD +67 -65
  43. mito_ai/tests/streamlit_conversion/test_apply_patch_to_text.py +0 -368
  44. mito_ai/tests/streamlit_conversion/test_fix_diff_headers.py +0 -533
  45. mito_ai-0.1.46.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.20f12766ecd3d430568e.js.map +0 -1
  46. /mito_ai/streamlit_conversion/{streamlit_system_prompt.py → prompts/streamlit_system_prompt.py} +0 -0
  47. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
  48. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +0 -0
  49. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +0 -0
  50. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +0 -0
  51. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
  52. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +0 -0
  53. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +0 -0
  54. {mito_ai-0.1.46.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
  55. {mito_ai-0.1.46.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
  56. {mito_ai-0.1.46.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
  57. {mito_ai-0.1.46.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
  58. {mito_ai-0.1.46.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
  59. {mito_ai-0.1.46.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
  60. {mito_ai-0.1.46.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
  61. {mito_ai-0.1.46.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
  62. {mito_ai-0.1.46.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
  63. {mito_ai-0.1.46.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
  64. {mito_ai-0.1.46.data → mito_ai-0.1.47.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +0 -0
  65. {mito_ai-0.1.46.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
  66. {mito_ai-0.1.46.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
  67. {mito_ai-0.1.46.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
  68. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/WHEEL +0 -0
  69. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/entry_points.txt +0 -0
  70. {mito_ai-0.1.46.dist-info → mito_ai-0.1.47.dist-info}/licenses/LICENSE +0 -0
@@ -2,44 +2,41 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  from typing import Tuple, Optional
5
- import os
6
- from mito_ai.streamlit_conversion.streamlit_utils import get_app_path
7
5
  from mito_ai.streamlit_conversion.streamlit_agent_handler import streamlit_handler
6
+ from mito_ai.path_utils import AbsoluteNotebookPath, does_app_path_exists, get_absolute_app_path, get_absolute_notebook_dir_path
7
+ from mito_ai.utils.error_classes import StreamlitPreviewError
8
8
 
9
9
 
10
- def validate_request_body(body: Optional[dict]) -> Tuple[bool, str, Optional[str], bool, str]:
10
+ def validate_request_body(body: Optional[dict]) -> Tuple[str, bool, str]:
11
11
  """Validate the request body and extract notebook_path and force_recreate."""
12
12
  if body is None:
13
- return False, "Invalid or missing JSON body", None, False, ""
13
+ raise StreamlitPreviewError("Invalid or missing JSON body", 400)
14
14
 
15
15
  notebook_path = body.get("notebook_path")
16
16
  if not notebook_path:
17
- return False, "Missing notebook_path parameter", None, False, ""
17
+ raise StreamlitPreviewError("Missing notebook_path parameter", 400)
18
18
 
19
19
  force_recreate = body.get("force_recreate", False)
20
20
  if not isinstance(force_recreate, bool):
21
- return False, "force_recreate must be a boolean", None, False, ""
21
+ raise StreamlitPreviewError("force_recreate must be a boolean", 400)
22
22
 
23
23
  edit_prompt = body.get("edit_prompt", "")
24
24
  if not isinstance(edit_prompt, str):
25
- return False, "edit_prompt must be a string", None, False, ""
25
+ raise StreamlitPreviewError("edit_prompt must be a string", 400)
26
26
 
27
- return True, "", notebook_path, force_recreate, edit_prompt
27
+ return notebook_path, force_recreate, edit_prompt
28
28
 
29
- async def ensure_app_exists(resolved_notebook_path: str, force_recreate: bool = False, edit_prompt: str = "") -> Tuple[bool, str]:
29
+ async def ensure_app_exists(absolute_notebook_path: AbsoluteNotebookPath, force_recreate: bool = False, edit_prompt: str = "") -> None:
30
30
  """Ensure app.py exists, generating it if necessary or if force_recreate is True."""
31
- # Check if the app already exists
32
- app_path = get_app_path(os.path.dirname(resolved_notebook_path))
31
+
32
+ absolute_notebook_dir_path = get_absolute_notebook_dir_path(absolute_notebook_path)
33
+ absolute_app_path = get_absolute_app_path(absolute_notebook_dir_path)
34
+ app_path_exists = does_app_path_exists(absolute_app_path)
33
35
 
34
- if app_path is None or force_recreate:
35
- if app_path is None:
36
+ if not app_path_exists or force_recreate:
37
+ if not app_path_exists:
36
38
  print("[Mito AI] App path not found, generating streamlit code")
37
39
  else:
38
40
  print("[Mito AI] Force recreating streamlit app")
39
41
 
40
- success, app_path, message = await streamlit_handler(resolved_notebook_path, edit_prompt)
41
-
42
- if not success or app_path is None:
43
- return False, f"Failed to generate streamlit code: {message}"
44
-
45
- return True, ""
42
+ await streamlit_handler(absolute_notebook_path, edit_prompt)
@@ -0,0 +1,226 @@
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.utils.error_classes import StreamlitConversionError
5
+ import pytest
6
+ from mito_ai.streamlit_conversion.search_replace_utils import apply_search_replace
7
+
8
+
9
+ @pytest.mark.parametrize("original_text,search_replace_pairs,expected_result", [
10
+ # Test case 1: Simple title change
11
+ (
12
+ """import streamlit as st
13
+
14
+ st.markdown(\"\"\"
15
+ <style>
16
+ #MainMenu {visibility: hidden;}
17
+ .stAppDeployButton {display:none;}
18
+ footer {visibility: hidden;}
19
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
20
+ </style>
21
+ \"\"\", unsafe_allow_html=True)
22
+
23
+ st.title("Simple Calculation")
24
+
25
+ x = 5
26
+ y = 10
27
+ result = x + y
28
+
29
+ st.write(f"x = {x}")
30
+ st.write(f"y = {y}")
31
+ st.write(f"x + y = {result}")""",
32
+ [("st.title(\"Simple Calculation\")", "st.title(\"Math Examples\")")],
33
+ """import streamlit as st
34
+
35
+ st.markdown(\"\"\"
36
+ <style>
37
+ #MainMenu {visibility: hidden;}
38
+ .stAppDeployButton {display:none;}
39
+ footer {visibility: hidden;}
40
+ .stMainBlockContainer {padding: 2rem 1rem 2rem 1rem;}
41
+ </style>
42
+ \"\"\", unsafe_allow_html=True)
43
+
44
+ st.title("Math Examples")
45
+
46
+ x = 5
47
+ y = 10
48
+ result = x + y
49
+
50
+ st.write(f"x = {x}")
51
+ st.write(f"y = {y}")
52
+ st.write(f"x + y = {result}")"""
53
+ ),
54
+
55
+ # Test case 2: Add new content
56
+ (
57
+ """import streamlit as st
58
+
59
+ st.title("My App")""",
60
+ [("st.title(\"My App\")", """st.title("My App")
61
+ st.header("Welcome")
62
+ st.write("This is a test app")""")],
63
+ """import streamlit as st
64
+
65
+ st.title("My App")
66
+ st.header("Welcome")
67
+ st.write("This is a test app")"""
68
+ ),
69
+
70
+ # Test case 3: Remove lines
71
+ (
72
+ """import streamlit as st
73
+
74
+ st.header("Welcome")
75
+ st.title("My App")
76
+ st.write("This is a test app")""",
77
+ [("""st.header("Welcome")
78
+ st.title("My App")
79
+ st.write("This is a test app")""", "st.title(\"My App\")")],
80
+ """import streamlit as st
81
+
82
+ st.title("My App")"""
83
+ ),
84
+
85
+ # Test case 4: Multiple replacements
86
+ (
87
+ """import streamlit as st
88
+
89
+ st.title("Old Title")
90
+ x = 5
91
+ y = 10
92
+ st.write("Old message")""",
93
+ [
94
+ ("st.title(\"Old Title\")", "st.title(\"New Title\")"),
95
+ ("st.write(\"Old message\")", "st.write(\"New message\")")
96
+ ],
97
+ """import streamlit as st
98
+
99
+ st.title("New Title")
100
+ x = 5
101
+ y = 10
102
+ st.write("New message")"""
103
+ ),
104
+
105
+ # Test case 5: Empty search/replace pairs
106
+ (
107
+ """import streamlit as st
108
+
109
+ st.title("My App")""",
110
+ [],
111
+ """import streamlit as st
112
+
113
+ st.title("My App")"""
114
+ ),
115
+
116
+ # Test case 6: Complex replacement with context
117
+ (
118
+ """import streamlit as st
119
+
120
+ # This is a comment
121
+ st.title("Old Title")
122
+ # Another comment
123
+ x = 5
124
+ y = 10
125
+ # Final comment""",
126
+ [("""# This is a comment
127
+ st.title("Old Title")
128
+ # Another comment""", """# This is a comment
129
+ st.title("New Title")
130
+ # Another comment""")],
131
+ """import streamlit as st
132
+
133
+ # This is a comment
134
+ st.title("New Title")
135
+ # Another comment
136
+ x = 5
137
+ y = 10
138
+ # Final comment"""
139
+ ),
140
+
141
+ # Test case 7: Replace multiple consecutive lines
142
+ (
143
+ """import streamlit as st
144
+
145
+ st.title("My App")
146
+ st.write("Line 1")
147
+ st.write("Line 2")
148
+ st.write("Line 3")
149
+
150
+ x = 5""",
151
+ [("""st.write("Line 1")
152
+ st.write("Line 2")
153
+ st.write("Line 3")""", "st.write(\"New content\")")],
154
+ """import streamlit as st
155
+
156
+ st.title("My App")
157
+ st.write("New content")
158
+
159
+ x = 5"""
160
+ ),
161
+
162
+ # Test case 8: Add lines at the beginning
163
+ (
164
+ """import streamlit as st
165
+
166
+ st.title("My App")""",
167
+ [("import streamlit as st", """import pandas as pd
168
+ import streamlit as st""")],
169
+ """import pandas as pd
170
+ import streamlit as st
171
+
172
+ st.title("My App")"""
173
+ ),
174
+
175
+ # Test case 9: Add lines at the end
176
+ (
177
+ """import streamlit as st
178
+
179
+ st.title("My App")""",
180
+ [("st.title(\"My App\")", """st.title("My App")
181
+
182
+ st.write("Footer content")
183
+ st.write("More footer")""")],
184
+ """import streamlit as st
185
+
186
+ st.title("My App")
187
+
188
+ st.write("Footer content")
189
+ st.write("More footer")"""
190
+ ),
191
+
192
+ # Test case 10: Add emoji to streamlit app title
193
+ (
194
+ """import streamlit as st
195
+
196
+ st.title("My App")
197
+ st.write("Welcome to my application")""",
198
+ [("st.title(\"My App\")", "st.title(\"🚀 My App\")")],
199
+ """import streamlit as st
200
+
201
+ st.title("🚀 My App")
202
+ st.write("Welcome to my application")"""
203
+ )
204
+ ])
205
+ def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
206
+ """Test the apply_search_replace function with various search/replace scenarios."""
207
+ result = apply_search_replace(original_text, search_replace_pairs)
208
+
209
+ print(f"Original text: {repr(original_text)}")
210
+ print(f"Search/replace pairs: {search_replace_pairs}")
211
+ print(f"Expected result: {repr(expected_result)}")
212
+ print(f"Result: {repr(result)}")
213
+
214
+ assert result == expected_result
215
+
216
+
217
+ def test_apply_search_replace_search_not_found():
218
+ """Test that ValueError is raised when search text is not found."""
219
+ with pytest.raises(StreamlitConversionError, match="Search text not found"):
220
+ apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
221
+
222
+
223
+ def test_apply_search_replace_multiple_matches():
224
+ """Test that ValueError is raised when search text is found multiple times."""
225
+ with pytest.raises(StreamlitConversionError, match="Search text found 2 times"):
226
+ apply_search_replace("st.write(\"Hello\")\nst.write(\"Hello\")", [("st.write(\"Hello\")", "st.write(\"Hi\")")])
@@ -12,7 +12,7 @@ from mito_ai.streamlit_conversion.streamlit_agent_handler import (
12
12
  correct_error_in_generation,
13
13
  streamlit_handler
14
14
  )
15
- from mito_ai.streamlit_conversion.streamlit_utils import clean_directory_check
15
+ from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookPath, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
16
16
 
17
17
  # Add this line to enable async support
18
18
  pytest_plugins = ('pytest_asyncio',)
@@ -102,14 +102,12 @@ class TestCorrectErrorInGeneration:
102
102
  @patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
103
103
  async def test_correct_error_in_generation_success(self, mock_stream):
104
104
  """Test successful error correction"""
105
- mock_response = """```unified_diff
106
- --- a/app.py
107
- +++ b/app.py
108
- @@ -1,1 +1,1 @@
109
- -import streamlit
110
- -st.title('Test')
111
- +import streamlit
112
- +st.title('Fixed')
105
+ mock_response = """```search_replace
106
+ >>>>>>> SEARCH
107
+ st.title('Test')
108
+ =======
109
+ st.title('Fixed')
110
+ <<<<<<< REPLACE
113
111
  ```"""
114
112
  async def mock_async_gen():
115
113
  for item in [mock_response]:
@@ -117,8 +115,8 @@ class TestCorrectErrorInGeneration:
117
115
 
118
116
  mock_stream.return_value = mock_async_gen()
119
117
 
120
- result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')")
121
-
118
+ result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
119
+
122
120
  expected_code = "import streamlit\nst.title('Fixed')\n"
123
121
  assert result == expected_code
124
122
 
@@ -140,8 +138,7 @@ class TestStreamlitHandler:
140
138
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
141
139
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
142
140
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
143
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
144
- async def test_streamlit_handler_success(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
141
+ async def test_streamlit_handler_success(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
145
142
  """Test successful streamlit handler execution"""
146
143
  # Mock notebook parsing
147
144
  mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
@@ -153,26 +150,19 @@ class TestStreamlitHandler:
153
150
  # Mock validation (no errors)
154
151
  mock_validator.return_value = (False, "")
155
152
 
156
- # Mock file creation
157
- mock_create_file.return_value = (True, "/path/to/app.py", "File created successfully")
158
-
159
- # Mock clean directory check (no-op)
160
- mock_clean_directory.return_value = None
161
-
162
153
  # Use a relative path that will work cross-platform
163
- notebook_path = "notebook.ipynb"
164
- result = await streamlit_handler(notebook_path)
154
+ notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
165
155
 
166
- assert result[0] is True
167
- assert "File created successfully" in result[2]
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)
168
160
 
169
161
  # Verify calls
170
162
  mock_parse.assert_called_once_with(notebook_path)
171
163
  mock_generate_code.assert_called_once_with(mock_notebook_data)
172
164
  mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
173
- # get_app_directory converts relative paths to absolute, so expect the absolute path directory
174
- expected_app_dir = os.path.dirname(os.path.abspath(notebook_path))
175
- mock_create_file.assert_called_once_with(expected_app_dir, "import streamlit\nst.title('Test')")
165
+ mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
176
166
 
177
167
  @pytest.mark.asyncio
178
168
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
@@ -192,11 +182,9 @@ class TestStreamlitHandler:
192
182
  # Mock validation (always errors) - Return list of errors as expected by validate_app
193
183
  mock_validator.return_value = (True, ["Persistent error"])
194
184
 
195
- result = await streamlit_handler("notebook.ipynb")
196
-
197
- # Verify the result indicates failure
198
- assert result[0] is False
199
- 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"))
200
188
 
201
189
  # Verify that error correction was called 5 times (max retries)
202
190
  assert mock_correct_error.call_count == 5
@@ -206,8 +194,7 @@ class TestStreamlitHandler:
206
194
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
207
195
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
208
196
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
209
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
210
- async def test_streamlit_handler_file_creation_failure(self, mock_clean_directory, mock_create_file, mock_validator, mock_generate_code, mock_parse):
197
+ async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
211
198
  """Test streamlit handler when file creation fails"""
212
199
  # Mock notebook parsing
213
200
  mock_notebook_data: List[dict] = [{"cells": []}]
@@ -219,35 +206,27 @@ class TestStreamlitHandler:
219
206
  # Mock validation (no errors)
220
207
  mock_validator.return_value = (False, "")
221
208
 
222
- # Mock file creation failure
223
- mock_create_file.return_value = (False, None, "Permission denied")
224
-
225
- # Mock clean directory check (no-op)
226
- 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")
227
211
 
228
- result = await streamlit_handler("notebook.ipynb")
229
-
230
- assert result[0] is False
231
- 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"))
232
215
 
233
216
  @pytest.mark.asyncio
234
217
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
235
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
236
- 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):
237
219
  """Test streamlit handler when notebook parsing fails"""
238
- # Mock clean directory check (no-op)
239
- mock_clean_directory.return_value = None
240
220
 
241
221
  mock_parse.side_effect = FileNotFoundError("Notebook not found")
242
222
 
243
223
  with pytest.raises(FileNotFoundError, match="Notebook not found"):
244
- await streamlit_handler("notebook.ipynb")
224
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
245
225
 
246
226
  @pytest.mark.asyncio
247
227
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
248
228
  @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
249
- @patch('mito_ai.streamlit_conversion.streamlit_agent_handler.clean_directory_check')
250
- async def test_streamlit_handler_generation_exception(self, mock_clean_directory, mock_generate_code, mock_parse):
229
+ async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
251
230
  """Test streamlit handler when code generation fails"""
252
231
  # Mock notebook parsing
253
232
  mock_notebook_data: List[dict] = [{"cells": []}]
@@ -256,11 +235,8 @@ class TestStreamlitHandler:
256
235
  # Mock code generation failure
257
236
  mock_generate_code.side_effect = Exception("Generation failed")
258
237
 
259
- # Mock clean directory check (no-op)
260
- mock_clean_directory.return_value = None
261
-
262
238
  with pytest.raises(Exception, match="Generation failed"):
263
- await streamlit_handler("notebook.ipynb")
239
+ await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"))
264
240
 
265
241
 
266
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
- 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 == []
@@ -11,6 +11,7 @@ from mito_ai.streamlit_conversion.validate_streamlit_app import (
11
11
  validate_app
12
12
  )
13
13
  import pytest
14
+ from mito_ai.path_utils import AbsoluteNotebookPath
14
15
 
15
16
 
16
17
  class TestGetSyntaxError:
@@ -56,7 +57,9 @@ class TestGetRuntimeErrors:
56
57
  ])
57
58
  def test_get_runtime_errors(self, app_code, expected_error):
58
59
  """Test getting runtime errors"""
59
- errors = get_runtime_errors(app_code, '/app.py')
60
+
61
+ absolute_path = AbsoluteNotebookPath('/notebook.ipynb')
62
+ errors = get_runtime_errors(app_code, absolute_path)
60
63
 
61
64
  if expected_error is None:
62
65
  assert errors is None
@@ -85,7 +88,7 @@ df=pd.read_csv('data.csv')
85
88
  with open(csv_path, "w") as f:
86
89
  f.write("name,age\nJohn,25\nJane,30")
87
90
 
88
- errors = 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
 
91
94
  class TestValidateApp:
@@ -99,7 +102,7 @@ class TestValidateApp:
99
102
  ])
100
103
  def test_validate_app(self, app_code, expected_has_validation_error, expected_error_message):
101
104
  """Test the validate_app function"""
102
- has_validation_error, errors = validate_app(app_code, '/app.py')
105
+ has_validation_error, errors = validate_app(app_code, AbsoluteNotebookPath('/notebook.ipynb'))
103
106
 
104
107
  assert has_validation_error == expected_has_validation_error
105
108
  assert expected_error_message in str(errors)