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
@@ -2,14 +2,14 @@
2
2
  # Distributed under the terms of the GNU Affero General Public License v3.0 License.
3
3
 
4
4
  import pytest
5
- from mito_ai.anthropic_client import get_anthropic_system_prompt_and_messages, extract_and_parse_anthropic_json_response, AnthropicClient
6
- from mito_ai.utils.anthropic_utils import get_anthropic_completion_function_params, FAST_ANTHROPIC_MODEL
5
+ from mito_ai.anthropic_client import get_anthropic_system_prompt_and_messages, get_anthropic_system_prompt_and_messages_with_caching, add_cache_control_to_message, extract_and_parse_anthropic_json_response, AnthropicClient
6
+ from mito_ai.utils.anthropic_utils import FAST_ANTHROPIC_MODEL
7
7
  from anthropic.types import Message, TextBlock, ToolUseBlock, Usage, ToolUseBlock, Message, Usage, TextBlock
8
8
  from openai.types.chat import ChatCompletionMessageParam, ChatCompletionUserMessageParam, ChatCompletionAssistantMessageParam, ChatCompletionSystemMessageParam
9
- from mito_ai.completions.models import MessageType, ResponseFormatInfo, AgentResponse
10
- from unittest.mock import MagicMock, patch
9
+ from mito_ai.completions.models import MessageType
10
+ from unittest.mock import patch
11
11
  import anthropic
12
- from typing import List, Dict, Any, cast, Union
12
+ from typing import List, Dict, cast
13
13
 
14
14
 
15
15
  # Dummy base64 image (1x1 PNG)
@@ -272,4 +272,176 @@ async def test_model_selection_based_on_message_type(message_type, expected_mode
272
272
  # Verify that create was called with the expected model
273
273
  mock_create.assert_called_once()
274
274
  call_args = mock_create.call_args
275
- assert call_args[1]['model'] == expected_model
275
+ assert call_args[1]['model'] == expected_model
276
+
277
+
278
+ # Caching Tests
279
+
280
+ @pytest.mark.parametrize("message,expected_role,expected_content_type,expected_content_length,expected_cache_control", [
281
+ # String content message
282
+ (
283
+ {"role": "user", "content": "Hello world"},
284
+ "user",
285
+ list,
286
+ 1,
287
+ True
288
+ ),
289
+ # List content message
290
+ (
291
+ {
292
+ "role": "user",
293
+ "content": [
294
+ {"type": "text", "text": "First part"},
295
+ {"type": "text", "text": "Second part"}
296
+ ]
297
+ },
298
+ "user",
299
+ list,
300
+ 2,
301
+ True
302
+ ),
303
+ # Empty content message
304
+ (
305
+ {"role": "user", "content": []},
306
+ "user",
307
+ list,
308
+ 0,
309
+ False
310
+ ),
311
+ # Assistant message with string content
312
+ (
313
+ {"role": "assistant", "content": "I can help you with that."},
314
+ "assistant",
315
+ list,
316
+ 1,
317
+ True
318
+ ),
319
+ ])
320
+ def test_add_cache_control_to_message(message, expected_role, expected_content_type, expected_content_length, expected_cache_control):
321
+ """Test adding cache control to different types of messages."""
322
+ result = add_cache_control_to_message(message)
323
+
324
+ assert result["role"] == expected_role
325
+ assert isinstance(result["content"], expected_content_type)
326
+ assert len(result["content"]) == expected_content_length
327
+
328
+ if expected_cache_control and expected_content_length > 0:
329
+ # Should have cache_control on the last content block
330
+ last_block = result["content"][-1]
331
+ assert last_block["cache_control"] == {"type": "ephemeral"}
332
+
333
+ # If there are multiple blocks, earlier blocks should not have cache_control
334
+ if expected_content_length > 1:
335
+ for i in range(expected_content_length - 1):
336
+ assert "cache_control" not in result["content"][i]
337
+ elif expected_content_length == 0:
338
+ # Empty content should return unchanged
339
+ assert result == message
340
+
341
+
342
+ @pytest.mark.parametrize("messages,expected_system_type,expected_system_content", [
343
+ # With system prompt
344
+ (
345
+ [
346
+ ChatCompletionSystemMessageParam(role="system", content="You are a helpful assistant."),
347
+ ChatCompletionUserMessageParam(role="user", content="Hello!")
348
+ ],
349
+ list,
350
+ "You are a helpful assistant.",
351
+ ),
352
+ # Without system prompt
353
+ (
354
+ [
355
+ ChatCompletionUserMessageParam(role="user", content="Hello!"),
356
+ ChatCompletionAssistantMessageParam(role="assistant", content="Hi there!")
357
+ ],
358
+ anthropic.Omit,
359
+ None,
360
+ ),
361
+ # Multiple system messages (should take last one)
362
+ (
363
+ [
364
+ ChatCompletionSystemMessageParam(role="system", content="First system message."),
365
+ ChatCompletionSystemMessageParam(role="system", content="Second system message."),
366
+ ChatCompletionUserMessageParam(role="user", content="Hello!"),
367
+ ChatCompletionUserMessageParam(role="user", content="Hello!"),
368
+ ChatCompletionUserMessageParam(role="user", content="Hello!")
369
+ ],
370
+ list,
371
+ "Second system message.",
372
+ ),
373
+ ])
374
+ def test_caching_system_prompt_scenarios(messages, expected_system_type, expected_system_content):
375
+ """Test caching with different system prompt scenarios."""
376
+ system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
377
+
378
+ # Check system prompt
379
+ assert isinstance(system_prompt, expected_system_type)
380
+ if expected_system_content:
381
+ assert system_prompt[0]["text"] == expected_system_content
382
+ assert system_prompt[0]["cache_control"] == {"type": "ephemeral"}
383
+
384
+
385
+ @pytest.mark.parametrize("message_count,expected_cache_boundary", [
386
+ (1, None), # 1 message, No cache boundary
387
+ (3, None), # 3 messages, No cache boundary
388
+ (5, 1), # 5 messages, cache at index 2
389
+ (10, 6), # 10 messages, cache at index 6
390
+ ])
391
+ def test_caching_conversation_history(message_count, expected_cache_boundary):
392
+ """Test that conversation history is cached at the keep_recent boundary for different message counts."""
393
+
394
+ # Create messages based on the parameter
395
+ messages: List[ChatCompletionMessageParam] = [
396
+ ChatCompletionSystemMessageParam(role="system", content="You are helpful.")
397
+ ]
398
+
399
+ # Add message pairs
400
+ for i in range(message_count):
401
+ messages.append(ChatCompletionUserMessageParam(role="user", content=f"Message {i+1}"))
402
+
403
+ system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
404
+
405
+ # System prompt should have cache control
406
+ assert isinstance(system_prompt, list)
407
+ assert system_prompt[0]["cache_control"] == {"type": "ephemeral"}
408
+
409
+ print(anthropic_messages)
410
+
411
+ if expected_cache_boundary is None:
412
+ # Verify no cache boundry
413
+ assert all("cache_control" not in str(message) for message in anthropic_messages)
414
+ else:
415
+ # Other messages should not have cache control
416
+ for i, message in enumerate(anthropic_messages):
417
+ if i == expected_cache_boundary:
418
+ assert anthropic_messages[expected_cache_boundary]["content"][0]["cache_control"] == {"type": "ephemeral"}
419
+ else:
420
+ assert "cache_control" not in str(message)
421
+
422
+ def test_caching_with_mixed_content():
423
+ """Test caching with mixed text and image content."""
424
+ messages: List[ChatCompletionMessageParam] = [
425
+ ChatCompletionSystemMessageParam(role="system", content="You are a helpful assistant."),
426
+ ChatCompletionUserMessageParam(role="user", content=[
427
+ {"type": "text", "text": "Here is an image:"},
428
+ {"type": "image_url", "image_url": {"url": DUMMY_IMAGE_DATA_URL}}
429
+ ])
430
+ ]
431
+ system_prompt, anthropic_messages = get_anthropic_system_prompt_and_messages_with_caching(messages)
432
+
433
+ # System prompt should have cache control
434
+ assert isinstance(system_prompt, list)
435
+ assert system_prompt[0]["cache_control"] == {"type": "ephemeral"}
436
+
437
+ # User message should NOT have cache control (only 1 message, so boundary is invalid)
438
+ user_message = anthropic_messages[0]
439
+ assert user_message["role"] == "user"
440
+ assert isinstance(user_message["content"], list)
441
+ assert len(user_message["content"]) == 2
442
+
443
+ # No content blocks should have cache control (too few messages to cache)
444
+ assert user_message["content"][0]["type"] == "text"
445
+ assert "cache_control" not in user_message["content"][0]
446
+ assert user_message["content"][1]["type"] == "image"
447
+ assert "cache_control" not in user_message["content"][1]
@@ -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\")")])