mito-ai 0.1.45__py3-none-any.whl → 0.1.47__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mito-ai might be problematic. Click here for more details.

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