mito-ai 0.1.33__py3-none-any.whl → 0.1.49__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.
- mito_ai/__init__.py +49 -9
- mito_ai/_version.py +1 -1
- mito_ai/anthropic_client.py +142 -67
- mito_ai/{app_builder → app_deploy}/__init__.py +1 -1
- mito_ai/app_deploy/app_deploy_utils.py +44 -0
- mito_ai/app_deploy/handlers.py +345 -0
- mito_ai/{app_builder → app_deploy}/models.py +35 -22
- mito_ai/app_manager/__init__.py +4 -0
- mito_ai/app_manager/handlers.py +167 -0
- mito_ai/app_manager/models.py +71 -0
- mito_ai/app_manager/utils.py +24 -0
- mito_ai/auth/README.md +18 -0
- mito_ai/auth/__init__.py +6 -0
- mito_ai/auth/handlers.py +96 -0
- mito_ai/auth/urls.py +13 -0
- mito_ai/chat_history/handlers.py +63 -0
- mito_ai/chat_history/urls.py +32 -0
- mito_ai/completions/completion_handlers/agent_execution_handler.py +1 -1
- mito_ai/completions/completion_handlers/chat_completion_handler.py +4 -4
- mito_ai/completions/completion_handlers/utils.py +99 -37
- mito_ai/completions/handlers.py +57 -20
- mito_ai/completions/message_history.py +9 -1
- mito_ai/completions/models.py +31 -7
- mito_ai/completions/prompt_builders/agent_execution_prompt.py +21 -2
- mito_ai/completions/prompt_builders/agent_smart_debug_prompt.py +8 -0
- mito_ai/completions/prompt_builders/agent_system_message.py +115 -42
- mito_ai/completions/prompt_builders/chat_name_prompt.py +6 -6
- mito_ai/completions/prompt_builders/chat_prompt.py +18 -11
- mito_ai/completions/prompt_builders/chat_system_message.py +4 -0
- mito_ai/completions/prompt_builders/prompt_constants.py +23 -4
- mito_ai/completions/prompt_builders/utils.py +72 -10
- mito_ai/completions/providers.py +81 -47
- mito_ai/constants.py +25 -24
- mito_ai/file_uploads/__init__.py +3 -0
- mito_ai/file_uploads/handlers.py +248 -0
- mito_ai/file_uploads/urls.py +21 -0
- mito_ai/gemini_client.py +44 -48
- mito_ai/log/handlers.py +10 -3
- mito_ai/log/urls.py +3 -3
- mito_ai/openai_client.py +30 -44
- mito_ai/path_utils.py +70 -0
- mito_ai/streamlit_conversion/agent_utils.py +37 -0
- mito_ai/streamlit_conversion/prompts/prompt_constants.py +172 -0
- mito_ai/streamlit_conversion/prompts/prompt_utils.py +10 -0
- mito_ai/streamlit_conversion/prompts/streamlit_app_creation_prompt.py +46 -0
- mito_ai/streamlit_conversion/prompts/streamlit_error_correction_prompt.py +28 -0
- mito_ai/streamlit_conversion/prompts/streamlit_finish_todo_prompt.py +45 -0
- mito_ai/streamlit_conversion/prompts/streamlit_system_prompt.py +56 -0
- mito_ai/streamlit_conversion/prompts/update_existing_app_prompt.py +50 -0
- mito_ai/streamlit_conversion/search_replace_utils.py +94 -0
- mito_ai/streamlit_conversion/streamlit_agent_handler.py +144 -0
- mito_ai/streamlit_conversion/streamlit_utils.py +85 -0
- mito_ai/streamlit_conversion/validate_streamlit_app.py +105 -0
- mito_ai/streamlit_preview/__init__.py +6 -0
- mito_ai/streamlit_preview/handlers.py +111 -0
- mito_ai/streamlit_preview/manager.py +152 -0
- mito_ai/streamlit_preview/urls.py +22 -0
- mito_ai/streamlit_preview/utils.py +29 -0
- mito_ai/tests/chat_history/test_chat_history.py +211 -0
- mito_ai/tests/completions/completion_handlers_utils_test.py +190 -0
- mito_ai/tests/deploy_app/test_app_deploy_utils.py +89 -0
- mito_ai/tests/file_uploads/__init__.py +2 -0
- mito_ai/tests/file_uploads/test_handlers.py +282 -0
- mito_ai/tests/message_history/test_generate_short_chat_name.py +0 -4
- mito_ai/tests/message_history/test_message_history_utils.py +103 -23
- mito_ai/tests/open_ai_utils_test.py +18 -22
- mito_ai/tests/providers/test_anthropic_client.py +447 -0
- mito_ai/tests/providers/test_azure.py +2 -6
- mito_ai/tests/providers/test_capabilities.py +120 -0
- mito_ai/tests/{test_gemini_client.py → providers/test_gemini_client.py} +40 -36
- mito_ai/tests/providers/test_mito_server_utils.py +448 -0
- mito_ai/tests/providers/test_model_resolution.py +130 -0
- mito_ai/tests/providers/test_openai_client.py +57 -0
- mito_ai/tests/providers/test_provider_completion_exception.py +66 -0
- mito_ai/tests/providers/test_provider_limits.py +42 -0
- mito_ai/tests/providers/test_providers.py +382 -0
- mito_ai/tests/providers/test_retry_logic.py +389 -0
- mito_ai/tests/providers/test_stream_mito_server_utils.py +140 -0
- mito_ai/tests/providers/utils.py +85 -0
- mito_ai/tests/streamlit_conversion/__init__.py +3 -0
- mito_ai/tests/streamlit_conversion/test_apply_search_replace.py +240 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_agent_handler.py +246 -0
- mito_ai/tests/streamlit_conversion/test_streamlit_utils.py +193 -0
- mito_ai/tests/streamlit_conversion/test_validate_streamlit_app.py +112 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_handler.py +118 -0
- mito_ai/tests/streamlit_preview/test_streamlit_preview_manager.py +292 -0
- mito_ai/tests/test_constants.py +31 -3
- mito_ai/tests/test_telemetry.py +12 -0
- mito_ai/tests/user/__init__.py +2 -0
- mito_ai/tests/user/test_user.py +120 -0
- mito_ai/tests/utils/test_anthropic_utils.py +6 -6
- mito_ai/user/handlers.py +45 -0
- mito_ai/user/urls.py +21 -0
- mito_ai/utils/anthropic_utils.py +55 -121
- mito_ai/utils/create.py +17 -1
- mito_ai/utils/error_classes.py +42 -0
- mito_ai/utils/gemini_utils.py +39 -94
- mito_ai/utils/message_history_utils.py +7 -4
- mito_ai/utils/mito_server_utils.py +242 -0
- mito_ai/utils/open_ai_utils.py +38 -155
- mito_ai/utils/provider_utils.py +49 -0
- mito_ai/utils/server_limits.py +1 -1
- mito_ai/utils/telemetry_utils.py +137 -5
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/build_log.json +102 -100
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/package.json +4 -2
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/package.json.orig +3 -1
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/schemas/mito_ai/toolbar-buttons.json +2 -2
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js +15948 -8403
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.8f1845da6bf2b128c049.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js +198 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/node_modules_process_browser_js.4b128e94d31a81ebd209.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js +58 -33
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.8b24b5b3b93f95205b56.js.map +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js +10 -2
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.5876024bb17dbd6a3ee6.js.map +1 -0
- mito_ai-0.1.49.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 +533 -0
- mito_ai-0.1.49.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 +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js +6941 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_auth_dist_esm_providers_cognito_tokenProvider_tokenProvider_-72f1c8.a917210f057fcfe224ad.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js +1021 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_dist_esm_index_mjs.6bac1a8c4cc93f15f6b7.js.map +1 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js +59698 -0
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_aws-amplify_ui-react_dist_esm_index_mjs.4fcecd65bef9e9847609.js.map +1 -0
- mito_ai-0.1.49.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 +7440 -0
- mito_ai-0.1.49.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 +1 -0
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js → mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js +2 -240
- mito_ai-0.1.49.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.3f6754ac5116d47de76b.js.map +1 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/METADATA +5 -2
- mito_ai-0.1.49.dist-info/RECORD +205 -0
- mito_ai/app_builder/handlers.py +0 -218
- mito_ai/tests/providers_test.py +0 -438
- mito_ai/tests/test_anthropic_client.py +0 -270
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/lib_index_js.281f4b9af60d620c6fb1.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/remoteEntry.4f1d00fd0c58fcc05d8d.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/style_index_js.06083e515de4862df010.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js +0 -7842
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_html2canvas_dist_html2canvas_js.ea47e8c8c906197f8d19.js.map +0 -1
- mito_ai-0.1.33.data/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_semver_index_js.9795f79265ddb416864b.js.map +0 -1
- mito_ai-0.1.33.dist-info/RECORD +0 -134
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/etc/jupyter/jupyter_server_config.d/mito_ai.json +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/style.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js +0 -0
- {mito_ai-0.1.33.data → mito_ai-0.1.49.data}/data/share/jupyter/labextensions/mito_ai/static/vendors-node_modules_vscode-diff_dist_index_js.ea55f1f9346638aafbcf.js.map +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/WHEEL +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/entry_points.txt +0 -0
- {mito_ai-0.1.33.dist-info → mito_ai-0.1.49.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
# Test case 11: Only replace first occurrence when search text exists multiple times
|
|
206
|
+
(
|
|
207
|
+
"""import streamlit as st
|
|
208
|
+
|
|
209
|
+
st.write("Hello World")
|
|
210
|
+
st.title("My App")
|
|
211
|
+
st.write("Hello World")
|
|
212
|
+
st.write("Another message")""",
|
|
213
|
+
[("st.write(\"Hello World\")", "st.write(\"Hi There\")")],
|
|
214
|
+
"""import streamlit as st
|
|
215
|
+
|
|
216
|
+
st.write("Hi There")
|
|
217
|
+
st.title("My App")
|
|
218
|
+
st.write("Hello World")
|
|
219
|
+
st.write("Another message")"""
|
|
220
|
+
)
|
|
221
|
+
])
|
|
222
|
+
def test_apply_search_replace(original_text, search_replace_pairs, expected_result):
|
|
223
|
+
"""Test the apply_search_replace function with various search/replace scenarios."""
|
|
224
|
+
result = apply_search_replace(original_text, search_replace_pairs)
|
|
225
|
+
|
|
226
|
+
print(f"Original text: {repr(original_text)}")
|
|
227
|
+
print(f"Search/replace pairs: {search_replace_pairs}")
|
|
228
|
+
print(f"Expected result: {repr(expected_result)}")
|
|
229
|
+
print(f"Result: {repr(result)}")
|
|
230
|
+
|
|
231
|
+
assert result == expected_result
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_apply_search_replace_search_not_found():
|
|
235
|
+
"""Test that ValueError is raised when search text is not found."""
|
|
236
|
+
with pytest.raises(StreamlitConversionError, match="Search text not found"):
|
|
237
|
+
apply_search_replace("st.title(\"My App\")", [("st.title(\"Not Found\")", "st.title(\"New Title\")")])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
from typing import List
|
|
5
|
+
from anthropic.types import MessageParam
|
|
6
|
+
import pytest
|
|
7
|
+
import os
|
|
8
|
+
from unittest.mock import patch, AsyncMock, MagicMock
|
|
9
|
+
from mito_ai.streamlit_conversion.streamlit_agent_handler import (
|
|
10
|
+
get_response_from_agent,
|
|
11
|
+
generate_new_streamlit_code,
|
|
12
|
+
correct_error_in_generation,
|
|
13
|
+
streamlit_handler
|
|
14
|
+
)
|
|
15
|
+
from mito_ai.path_utils import AbsoluteNotebookPath, AppFileName, get_absolute_app_path, get_absolute_notebook_dir_path, get_absolute_notebook_path
|
|
16
|
+
|
|
17
|
+
# Add this line to enable async support
|
|
18
|
+
pytest_plugins = ('pytest_asyncio',)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestGetResponseFromAgent:
|
|
22
|
+
"""Test cases for get_response_from_agent function"""
|
|
23
|
+
|
|
24
|
+
@pytest.mark.asyncio
|
|
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"""
|
|
28
|
+
# Mock the async generator
|
|
29
|
+
async def mock_async_gen():
|
|
30
|
+
yield "Here's your code:\n```python\nimport streamlit\nst.title('Test')\n```"
|
|
31
|
+
|
|
32
|
+
mock_stream.return_value = mock_async_gen()
|
|
33
|
+
|
|
34
|
+
messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
|
|
35
|
+
response = await get_response_from_agent(messages)
|
|
36
|
+
|
|
37
|
+
assert response is not None
|
|
38
|
+
assert len(response) > 0
|
|
39
|
+
assert "import streamlit" in response
|
|
40
|
+
|
|
41
|
+
@pytest.mark.asyncio
|
|
42
|
+
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
43
|
+
@pytest.mark.parametrize("mock_items,expected_result", [
|
|
44
|
+
(["Hello", " World", "!"], "Hello World!"),
|
|
45
|
+
([], ""),
|
|
46
|
+
(["Here's your code: import streamlit"], "Here's your code: import streamlit")
|
|
47
|
+
])
|
|
48
|
+
async def test_get_response_from_agent_parametrized(self, mock_stream, mock_items, expected_result):
|
|
49
|
+
"""Test response from agent with different scenarios"""
|
|
50
|
+
# Mock the async generator
|
|
51
|
+
async def mock_async_gen():
|
|
52
|
+
for item in mock_items:
|
|
53
|
+
yield item
|
|
54
|
+
|
|
55
|
+
mock_stream.return_value = mock_async_gen()
|
|
56
|
+
|
|
57
|
+
messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
|
|
58
|
+
result = await get_response_from_agent(messages)
|
|
59
|
+
|
|
60
|
+
assert result == expected_result
|
|
61
|
+
mock_stream.assert_called_once()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
66
|
+
async def test_get_response_from_agent_exception(self, mock_stream):
|
|
67
|
+
"""Test exception handling in get_response_from_agent"""
|
|
68
|
+
mock_stream.side_effect = Exception("API Error")
|
|
69
|
+
|
|
70
|
+
messages: List[MessageParam] = [{"role": "user", "content": [{"type": "text", "text": "test"}]}]
|
|
71
|
+
|
|
72
|
+
with pytest.raises(Exception, match="API Error"):
|
|
73
|
+
await get_response_from_agent(messages)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestGenerateStreamlitCode:
|
|
77
|
+
"""Test cases for generate_new_streamlit_code function"""
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
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):
|
|
82
|
+
"""Test successful streamlit code generation"""
|
|
83
|
+
mock_response = "Here's your code:\n```python\nimport streamlit\nst.title('Hello')\n```"
|
|
84
|
+
|
|
85
|
+
async def mock_async_gen():
|
|
86
|
+
for item in [mock_response]:
|
|
87
|
+
yield item
|
|
88
|
+
|
|
89
|
+
mock_stream.return_value = mock_async_gen()
|
|
90
|
+
|
|
91
|
+
notebook_data: List[dict] = [{"cells": []}]
|
|
92
|
+
result = await generate_new_streamlit_code(notebook_data)
|
|
93
|
+
|
|
94
|
+
expected_code = "import streamlit\nst.title('Hello')\n"
|
|
95
|
+
assert result == expected_code
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestCorrectErrorInGeneration:
|
|
99
|
+
"""Test cases for correct_error_in_generation function"""
|
|
100
|
+
|
|
101
|
+
@pytest.mark.asyncio
|
|
102
|
+
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
103
|
+
async def test_correct_error_in_generation_success(self, mock_stream):
|
|
104
|
+
"""Test successful error correction"""
|
|
105
|
+
mock_response = """```search_replace
|
|
106
|
+
>>>>>>> SEARCH
|
|
107
|
+
st.title('Test')
|
|
108
|
+
=======
|
|
109
|
+
st.title('Fixed')
|
|
110
|
+
<<<<<<< REPLACE
|
|
111
|
+
```"""
|
|
112
|
+
async def mock_async_gen():
|
|
113
|
+
for item in [mock_response]:
|
|
114
|
+
yield item
|
|
115
|
+
|
|
116
|
+
mock_stream.return_value = mock_async_gen()
|
|
117
|
+
|
|
118
|
+
result = await correct_error_in_generation("ImportError: No module named 'pandas'", "import streamlit\nst.title('Test')\n")
|
|
119
|
+
|
|
120
|
+
expected_code = "import streamlit\nst.title('Fixed')\n"
|
|
121
|
+
assert result == expected_code
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
@patch('mito_ai.streamlit_conversion.agent_utils.stream_anthropic_completion_from_mito_server')
|
|
125
|
+
async def test_correct_error_in_generation_exception(self, mock_stream):
|
|
126
|
+
"""Test exception handling in error correction"""
|
|
127
|
+
mock_stream.side_effect = Exception("API Error")
|
|
128
|
+
|
|
129
|
+
with pytest.raises(Exception, match="API Error"):
|
|
130
|
+
await correct_error_in_generation("Some error", "import streamlit\nst.title('Test')")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TestStreamlitHandler:
|
|
134
|
+
"""Test cases for streamlit_handler function"""
|
|
135
|
+
|
|
136
|
+
@pytest.mark.asyncio
|
|
137
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
138
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
139
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
140
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
141
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_conversion_success')
|
|
142
|
+
async def test_streamlit_handler_success(self, mock_log_success, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
143
|
+
"""Test successful streamlit handler execution"""
|
|
144
|
+
# Mock notebook parsing
|
|
145
|
+
mock_notebook_data: List[dict] = [{"cells": [{"cell_type": "code", "source": ["import pandas"]}]}]
|
|
146
|
+
mock_parse.return_value = mock_notebook_data
|
|
147
|
+
|
|
148
|
+
# Mock code generation
|
|
149
|
+
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
150
|
+
|
|
151
|
+
# Mock validation (no errors)
|
|
152
|
+
mock_validator.return_value = []
|
|
153
|
+
|
|
154
|
+
# Use a relative path that will work cross-platform
|
|
155
|
+
notebook_path = AbsoluteNotebookPath("absolute/path/to/notebook.ipynb")
|
|
156
|
+
app_file_name = AppFileName('test-app-file-name.py')
|
|
157
|
+
|
|
158
|
+
# Construct the expected app path using the same method as the production code
|
|
159
|
+
app_directory = get_absolute_notebook_dir_path(notebook_path)
|
|
160
|
+
expected_app_path = get_absolute_app_path(app_directory, app_file_name)
|
|
161
|
+
await streamlit_handler(notebook_path, app_file_name)
|
|
162
|
+
|
|
163
|
+
# Verify calls
|
|
164
|
+
mock_parse.assert_called_once_with(notebook_path)
|
|
165
|
+
mock_generate_code.assert_called_once_with(mock_notebook_data)
|
|
166
|
+
mock_validator.assert_called_once_with("import streamlit\nst.title('Test')", notebook_path)
|
|
167
|
+
mock_create_file.assert_called_once_with(expected_app_path, "import streamlit\nst.title('Test')")
|
|
168
|
+
|
|
169
|
+
@pytest.mark.asyncio
|
|
170
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
171
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
172
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.correct_error_in_generation')
|
|
173
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
174
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.log_streamlit_app_validation_retry')
|
|
175
|
+
async def test_streamlit_handler_max_retries_exceeded(self, mock_log_retry, mock_validator, mock_correct_error, mock_generate_code, mock_parse):
|
|
176
|
+
"""Test streamlit handler when max retries are exceeded"""
|
|
177
|
+
# Mock notebook parsing
|
|
178
|
+
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
179
|
+
mock_parse.return_value = mock_notebook_data
|
|
180
|
+
|
|
181
|
+
# Mock code generation
|
|
182
|
+
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
183
|
+
mock_correct_error.return_value = "import streamlit\nst.title('Fixed')"
|
|
184
|
+
|
|
185
|
+
# Mock validation (always errors) - validate_app returns List[str]
|
|
186
|
+
mock_validator.return_value = ["Persistent error"]
|
|
187
|
+
|
|
188
|
+
# Now it should raise an exception instead of returning a tuple
|
|
189
|
+
with pytest.raises(Exception):
|
|
190
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
191
|
+
|
|
192
|
+
# Verify that error correction was called 5 times (once per error, 5 retries)
|
|
193
|
+
# Each retry processes 1 error, so 5 retries = 5 calls
|
|
194
|
+
assert mock_correct_error.call_count == 5
|
|
195
|
+
|
|
196
|
+
@pytest.mark.asyncio
|
|
197
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
198
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
199
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.validate_app')
|
|
200
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.create_app_file')
|
|
201
|
+
async def test_streamlit_handler_file_creation_failure(self, mock_create_file, mock_validator, mock_generate_code, mock_parse):
|
|
202
|
+
"""Test streamlit handler when file creation fails"""
|
|
203
|
+
# Mock notebook parsing
|
|
204
|
+
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
205
|
+
mock_parse.return_value = mock_notebook_data
|
|
206
|
+
|
|
207
|
+
# Mock code generation
|
|
208
|
+
mock_generate_code.return_value = "import streamlit\nst.title('Test')"
|
|
209
|
+
|
|
210
|
+
# Mock validation (no errors) - validate_app returns List[str]
|
|
211
|
+
mock_validator.return_value = []
|
|
212
|
+
|
|
213
|
+
# Mock file creation failure - now it should raise an exception
|
|
214
|
+
mock_create_file.side_effect = Exception("Permission denied")
|
|
215
|
+
|
|
216
|
+
# Now it should raise an exception instead of returning a tuple
|
|
217
|
+
with pytest.raises(Exception):
|
|
218
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
219
|
+
|
|
220
|
+
@pytest.mark.asyncio
|
|
221
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
222
|
+
async def test_streamlit_handler_parse_notebook_exception(self, mock_parse):
|
|
223
|
+
"""Test streamlit handler when notebook parsing fails"""
|
|
224
|
+
|
|
225
|
+
mock_parse.side_effect = FileNotFoundError("Notebook not found")
|
|
226
|
+
|
|
227
|
+
with pytest.raises(FileNotFoundError, match="Notebook not found"):
|
|
228
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
229
|
+
|
|
230
|
+
@pytest.mark.asyncio
|
|
231
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.parse_jupyter_notebook_to_extract_required_content')
|
|
232
|
+
@patch('mito_ai.streamlit_conversion.streamlit_agent_handler.generate_new_streamlit_code')
|
|
233
|
+
async def test_streamlit_handler_generation_exception(self, mock_generate_code, mock_parse):
|
|
234
|
+
"""Test streamlit handler when code generation fails"""
|
|
235
|
+
# Mock notebook parsing
|
|
236
|
+
mock_notebook_data: List[dict] = [{"cells": []}]
|
|
237
|
+
mock_parse.return_value = mock_notebook_data
|
|
238
|
+
|
|
239
|
+
# Mock code generation failure
|
|
240
|
+
mock_generate_code.side_effect = Exception("Generation failed")
|
|
241
|
+
|
|
242
|
+
with pytest.raises(Exception, match="Generation failed"):
|
|
243
|
+
await streamlit_handler(AbsoluteNotebookPath("notebook.ipynb"), AppFileName('test-app-file-name.py'))
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Copyright (c) Saga Inc.
|
|
2
|
+
# Distributed under the terms of the GNU Affero General Public License v3.0 License.
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
import json
|
|
6
|
+
import tempfile
|
|
7
|
+
import os
|
|
8
|
+
from unittest.mock import patch, mock_open
|
|
9
|
+
from mito_ai.streamlit_conversion.streamlit_utils import (
|
|
10
|
+
extract_code_blocks,
|
|
11
|
+
create_app_file,
|
|
12
|
+
parse_jupyter_notebook_to_extract_required_content
|
|
13
|
+
)
|
|
14
|
+
from mito_ai.path_utils import AbsoluteAppPath, AbsoluteNotebookDirPath, AbsoluteNotebookPath, get_absolute_notebook_path
|
|
15
|
+
from typing import Dict, Any
|
|
16
|
+
|
|
17
|
+
class TestExtractCodeBlocks:
|
|
18
|
+
"""Test cases for extract_code_blocks function"""
|
|
19
|
+
|
|
20
|
+
def test_extract_code_blocks_with_python_blocks(self):
|
|
21
|
+
"""Test extracting code from message with python code blocks"""
|
|
22
|
+
message = "Here's some code:\n```python\nimport streamlit\nst.title('Hello')\n```\nThat's it!"
|
|
23
|
+
result = extract_code_blocks(message)
|
|
24
|
+
expected = "import streamlit\nst.title('Hello')\n"
|
|
25
|
+
assert result == expected
|
|
26
|
+
|
|
27
|
+
def test_extract_code_blocks_without_python_blocks(self):
|
|
28
|
+
"""Test when message doesn't contain python code blocks"""
|
|
29
|
+
message = "This is just regular text without code blocks"
|
|
30
|
+
result = extract_code_blocks(message)
|
|
31
|
+
assert result == message
|
|
32
|
+
|
|
33
|
+
def test_extract_code_blocks_empty_message(self):
|
|
34
|
+
"""Test with empty message"""
|
|
35
|
+
message = ""
|
|
36
|
+
result = extract_code_blocks(message)
|
|
37
|
+
assert result == message
|
|
38
|
+
|
|
39
|
+
def test_extract_code_blocks_multiple_blocks(self):
|
|
40
|
+
"""Test extracting from first python block when multiple exist"""
|
|
41
|
+
message = "```python\ncode1\n```\n```python\ncode2\n```"
|
|
42
|
+
result = extract_code_blocks(message)
|
|
43
|
+
expected = "code1\n\ncode2\n"
|
|
44
|
+
assert result == expected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestCreateAppFile:
|
|
48
|
+
"""Test cases for create_app_file function"""
|
|
49
|
+
|
|
50
|
+
def test_create_app_file_success(self, tmp_path):
|
|
51
|
+
"""Test successful file creation"""
|
|
52
|
+
app_path = os.path.join(str(tmp_path), "app.py")
|
|
53
|
+
code = "import streamlit\nst.title('Test')"
|
|
54
|
+
|
|
55
|
+
create_app_file(AbsoluteAppPath(app_path), code)
|
|
56
|
+
|
|
57
|
+
assert app_path is not None
|
|
58
|
+
assert os.path.exists(app_path)
|
|
59
|
+
|
|
60
|
+
# Verify file was created with correct content
|
|
61
|
+
with open(app_path, 'r') as f:
|
|
62
|
+
content = f.read()
|
|
63
|
+
assert content == code
|
|
64
|
+
|
|
65
|
+
def test_create_app_file_io_error(self):
|
|
66
|
+
"""Test file creation with IO error"""
|
|
67
|
+
file_path = AbsoluteAppPath("/nonexistent/path/that/should/fail")
|
|
68
|
+
code = "import streamlit"
|
|
69
|
+
|
|
70
|
+
with pytest.raises(Exception):
|
|
71
|
+
create_app_file(file_path, code)
|
|
72
|
+
|
|
73
|
+
@patch('builtins.open', side_effect=Exception("Unexpected error"))
|
|
74
|
+
def test_create_app_file_unexpected_error(self, mock_open):
|
|
75
|
+
"""Test file creation with unexpected error"""
|
|
76
|
+
app_path = AbsoluteAppPath("/tmp/test")
|
|
77
|
+
code = "import streamlit"
|
|
78
|
+
|
|
79
|
+
with pytest.raises(Exception, match="Unexpected error"):
|
|
80
|
+
create_app_file(app_path, code)
|
|
81
|
+
|
|
82
|
+
def test_create_app_file_empty_code(self, tmp_path):
|
|
83
|
+
"""Test creating file with empty code"""
|
|
84
|
+
app_path = AbsoluteAppPath(os.path.join(str(tmp_path), "app.py"))
|
|
85
|
+
code = ""
|
|
86
|
+
|
|
87
|
+
create_app_file(app_path, code)
|
|
88
|
+
|
|
89
|
+
assert app_path is not None
|
|
90
|
+
assert os.path.exists(app_path)
|
|
91
|
+
|
|
92
|
+
with open(app_path, 'r') as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
assert content == ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestParseJupyterNotebookToExtractRequiredContent:
|
|
98
|
+
"""Test cases for parse_jupyter_notebook_to_extract_required_content function"""
|
|
99
|
+
|
|
100
|
+
def test_parse_valid_notebook(self, tmp_path):
|
|
101
|
+
"""Test parsing a valid notebook with cells"""
|
|
102
|
+
notebook_data: Dict[str, Any] = {
|
|
103
|
+
"cells": [
|
|
104
|
+
{
|
|
105
|
+
"cell_type": "code",
|
|
106
|
+
"source": ["import pandas as pd\n", "df = pd.DataFrame()\n"],
|
|
107
|
+
"metadata": {"some": "metadata"},
|
|
108
|
+
"execution_count": 1
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"cell_type": "markdown",
|
|
112
|
+
"source": ["# Title\n", "Some text\n"],
|
|
113
|
+
"metadata": {"another": "metadata"}
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"metadata": {"notebook_metadata": "value"},
|
|
117
|
+
"nbformat": 4
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
notebook_path = tmp_path / "test.ipynb"
|
|
121
|
+
with open(notebook_path, 'w') as f:
|
|
122
|
+
json.dump(notebook_data, f)
|
|
123
|
+
|
|
124
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
125
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
126
|
+
|
|
127
|
+
# Check that only cell_type and source are preserved
|
|
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]
|
|
133
|
+
|
|
134
|
+
assert result[1]['cell_type'] == 'markdown'
|
|
135
|
+
assert result[1]['source'] == ["# Title\n", "Some text\n"]
|
|
136
|
+
assert 'metadata' not in result[1]
|
|
137
|
+
|
|
138
|
+
def test_parse_notebook_file_not_found(self):
|
|
139
|
+
"""Test parsing non-existent notebook file"""
|
|
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"))
|
|
143
|
+
|
|
144
|
+
def test_parse_notebook_with_missing_cell_fields(self, tmp_path):
|
|
145
|
+
"""Test parsing notebook where cells are missing cell_type or source"""
|
|
146
|
+
notebook_data: Dict[str, Any] = {
|
|
147
|
+
"cells": [
|
|
148
|
+
{
|
|
149
|
+
"cell_type": "code"
|
|
150
|
+
# Missing source field
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"source": ["some text"]
|
|
154
|
+
# Missing cell_type field
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"cell_type": "markdown",
|
|
158
|
+
"source": ["# Title"]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
notebook_path = tmp_path / "test.ipynb"
|
|
164
|
+
with open(notebook_path, 'w') as f:
|
|
165
|
+
json.dump(notebook_data, f)
|
|
166
|
+
|
|
167
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
168
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
169
|
+
|
|
170
|
+
assert len(result) == 3
|
|
171
|
+
assert result[0]['cell_type'] == 'code'
|
|
172
|
+
assert result[0]['source'] == [] # Default empty list
|
|
173
|
+
|
|
174
|
+
assert result[1]['cell_type'] == '' # Default empty string
|
|
175
|
+
assert result[1]['source'] == ["some text"]
|
|
176
|
+
|
|
177
|
+
assert result[2]['cell_type'] == 'markdown'
|
|
178
|
+
assert result[2]['source'] == ["# Title"]
|
|
179
|
+
|
|
180
|
+
def test_parse_empty_notebook(self, tmp_path):
|
|
181
|
+
"""Test parsing notebook with empty cells list"""
|
|
182
|
+
notebook_data: Dict[str, Any] = {
|
|
183
|
+
"cells": []
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
notebook_path = tmp_path / "test.ipynb"
|
|
187
|
+
with open(notebook_path, 'w') as f:
|
|
188
|
+
json.dump(notebook_data, f)
|
|
189
|
+
|
|
190
|
+
absolute_path = get_absolute_notebook_path(str(notebook_path))
|
|
191
|
+
result = parse_jupyter_notebook_to_extract_required_content(absolute_path)
|
|
192
|
+
|
|
193
|
+
assert result == []
|