Skip to content

Commit 05ffefa

Browse files
authored
Upgrade Streamlit to 1.49 (#278)
* update streamlit * fix md5 error * fix sidebar updates * remove workspace selector if offline * update workspace via query param * fix duplicate component bug * fix log refresh * stream output in workflows * fix workflow display
1 parent 951c90c commit 05ffefa

7 files changed

Lines changed: 158 additions & 73 deletions

File tree

content/digest.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,4 @@ def main():
315315
st.error("Please check your input and try again. If the problem persists, try with a simpler enzyme like Trypsin.")
316316

317317

318-
if __name__ == "__main__":
319-
320-
main()
321318
main()

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ six==1.17.0
111111
# via python-dateutil
112112
smmap==5.0.2
113113
# via gitdb
114-
streamlit==1.43.0
114+
streamlit==1.49.0
115115
# via
116116
# src (pyproject.toml)
117117
# streamlit-js-eval

src/common/captcha_.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
from pathlib import Path
21
import streamlit as st
32
import streamlit.components.v1 as st_components
4-
from streamlit.source_util import page_icon_and_name, calc_md5, get_pages, _on_pages_changed
53

4+
from streamlit.source_util import page_icon_and_name
65
from captcha.image import ImageCaptcha
6+
from pathlib import Path
77

8+
import hashlib
89
import random
910
import string
1011
import os
1112

13+
def calc_md5(string : str):
14+
return hashlib.md5(string.encode()).hexdigest()
15+
16+
def get_pages():
17+
return st.runtime.get_pages()
18+
19+
def set_pages(pages : dict):
20+
st.runtime.set_pages(pages)
1221

1322
def delete_all_pages(main_script_path_str: str) -> None:
1423
"""
@@ -22,7 +31,7 @@ def delete_all_pages(main_script_path_str: str) -> None:
2231
2332
"""
2433
# Get all pages from the app's configuration
25-
current_pages = get_pages(main_script_path_str)
34+
current_pages = get_pages()
2635

2736
# Create a list to store keys pages to delete
2837
keys_to_delete = []
@@ -37,7 +46,7 @@ def delete_all_pages(main_script_path_str: str) -> None:
3746
del current_pages[key]
3847

3948
# Refresh the pages configuration
40-
_on_pages_changed.send()
49+
set_pages(current_pages)
4150

4251

4352
def delete_page(main_script_path_str: str, page_name: str) -> None:
@@ -52,15 +61,15 @@ def delete_page(main_script_path_str: str, page_name: str) -> None:
5261
None
5362
"""
5463
# Get all pages
55-
current_pages = get_pages(main_script_path_str)
64+
current_pages = get_pages()
5665

5766
# Iterate over all pages and delete the desired page if found
5867
for key, value in current_pages.items():
5968
if value["page_name"] == page_name:
6069
del current_pages[key]
6170

6271
# Refresh the pages configuration
63-
_on_pages_changed.send()
72+
set_pages(current_pages)
6473

6574

6675
def restore_all_pages(main_script_path_str: str) -> None:
@@ -74,7 +83,7 @@ def restore_all_pages(main_script_path_str: str) -> None:
7483
None
7584
"""
7685
# Get all pages
77-
pages = get_pages(main_script_path_str)
86+
pages = get_pages()
7887

7988
# Obtain the path to the main script
8089
main_script_path = Path(main_script_path_str)
@@ -126,7 +135,7 @@ def restore_all_pages(main_script_path_str: str) -> None:
126135
}
127136

128137
# Refresh the page configuration
129-
_on_pages_changed.send()
138+
set_pages(pages)
130139

131140

132141
def add_page(main_script_path_str: str, page_name: str) -> None:
@@ -141,7 +150,7 @@ def add_page(main_script_path_str: str, page_name: str) -> None:
141150
None
142151
"""
143152
# Get all pages
144-
pages = get_pages(main_script_path_str)
153+
pages = get_pages()
145154

146155
# Obtain the path to the main script
147156
main_script_path = Path(main_script_path_str)
@@ -168,7 +177,7 @@ def add_page(main_script_path_str: str, page_name: str) -> None:
168177
}
169178

170179
# Refresh the page configuration
171-
_on_pages_changed.send()
180+
set_pages(pages)
172181

173182

174183
length_captcha = 5

src/common/common.py

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import json
21
import os
3-
import shutil
42
import sys
53
import uuid
4+
import json
65
import time
6+
import psutil
7+
import shutil
8+
9+
import pandas as pd
10+
import streamlit as st
11+
712
from typing import Any
813
from pathlib import Path
914
from streamlit.components.v1 import html
1015

11-
import streamlit as st
12-
import pandas as pd
13-
import psutil
14-
1516
try:
1617
from tkinter import Tk, filedialog
1718

@@ -310,28 +311,25 @@ def render_sidebar(page: str = "") -> None:
310311
# The main page has workspace switcher
311312
# Display workspace switcher if workspace is enabled in local mode
312313
if st.session_state.settings["enable_workspaces"]:
313-
with st.expander("🖥️ **Workspaces**"):
314-
# Workspaces directory specified in the settings.json
315-
if (
316-
st.session_state.settings["workspaces_dir"]
317-
and st.session_state.location == "local"
318-
):
319-
workspaces_dir = Path(
320-
st.session_state.settings["workspaces_dir"],
321-
"workspaces-" + st.session_state.settings["repository-name"],
322-
)
323-
else:
324-
workspaces_dir = ".."
325-
# Online: show current workspace name in info text and option to change to other existing workspace
326-
if st.session_state.location == "local":
314+
# Workspaces directory specified in the settings.json
315+
if (
316+
st.session_state.settings["workspaces_dir"]
317+
and st.session_state.location == "local"
318+
):
319+
workspaces_dir = Path(
320+
st.session_state.settings["workspaces_dir"],
321+
"workspaces-" + st.session_state.settings["repository-name"],
322+
)
323+
else:
324+
workspaces_dir = ".."
325+
# Online: show current workspace name in info text and option to change to other existing workspace
326+
if st.session_state.location == "local":
327+
with st.expander("🖥️ **Workspaces**"):
327328
# Define callback function to change workspace
328329
def change_workspace():
329330
for key in params.keys():
330331
if key in st.session_state.keys():
331332
del st.session_state[key]
332-
st.session_state.workspace = Path(
333-
workspaces_dir, st.session_state["chosen-workspace"]
334-
)
335333
st.query_params.workspace = st.session_state["chosen-workspace"]
336334

337335
# Get all available workspaces as options

src/workflow/CommandExecutor.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -77,34 +77,85 @@ def run_command(self, command: list[str]) -> None:
7777
self.logger.log(f"Running command:\n"+' '.join(command)+"\nWaiting for command to finish...", 1)
7878
start_time = time.time()
7979

80-
# Execute the command
81-
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
80+
# Execute the command with real-time output capture
81+
process = subprocess.Popen(
82+
command,
83+
stdout=subprocess.PIPE,
84+
stderr=subprocess.PIPE,
85+
text=True,
86+
bufsize=1, # Line buffered
87+
universal_newlines=True
88+
)
8289
child_pid = process.pid
8390

8491
# Record the PID to keep track of running processes associated with this workspace/workflow
8592
# User can close the Streamlit app and return to a running workflow later
8693
pid_file_path = self.pid_dir / str(child_pid)
8794
pid_file_path.touch()
8895

89-
# Wait for command completion and capture output
90-
stdout, stderr = process.communicate()
96+
# Real-time output capture
97+
self._stream_output(process)
98+
99+
# Wait for process completion
100+
process.wait()
91101

92102
# Cleanup PID file
93103
pid_file_path.unlink()
94104

95105
end_time = time.time()
96106
execution_time = end_time - start_time
97-
# Format the logging prefix
107+
108+
# Log completion
98109
self.logger.log(f"Process finished:\n"+' '.join(command)+f"\nTotal time to run command: {execution_time:.2f} seconds", 1)
99110

100-
# Log stdout if present
101-
if stdout:
102-
self.logger.log(stdout.decode(), 2)
111+
# Check for errors
112+
if process.returncode != 0:
113+
self.logger.log(f"ERRORS OCCURRED: Process exited with code {process.returncode}", 2)
114+
115+
def _stream_output(self, process: subprocess.Popen) -> None:
116+
"""
117+
Streams stdout and stderr from a running process in real-time to the logger.
118+
This method runs in the workflow process, not the GUI thread, so it's safe to block.
119+
120+
Args:
121+
process: The subprocess.Popen object to stream from
122+
"""
123+
def read_stdout():
124+
"""Read stdout in real-time"""
125+
try:
126+
for line in iter(process.stdout.readline, ''):
127+
if line:
128+
self.logger.log(line.rstrip(), 2)
129+
if process.poll() is not None:
130+
break
131+
except Exception as e:
132+
self.logger.log(f"Error reading stdout: {e}", 2)
133+
finally:
134+
process.stdout.close()
135+
136+
def read_stderr():
137+
"""Read stderr in real-time"""
138+
try:
139+
for line in iter(process.stderr.readline, ''):
140+
if line:
141+
self.logger.log(f"STDERR: {line.rstrip()}", 2)
142+
if process.poll() is not None:
143+
break
144+
except Exception as e:
145+
self.logger.log(f"Error reading stderr: {e}", 2)
146+
finally:
147+
process.stderr.close()
148+
149+
# Start threads to read stdout and stderr simultaneously
150+
stdout_thread = threading.Thread(target=read_stdout, daemon=True)
151+
stderr_thread = threading.Thread(target=read_stderr, daemon=True)
152+
153+
stdout_thread.start()
154+
stderr_thread.start()
103155

104-
# Log stderr and raise an exception if errors occurred
105-
if stderr or process.returncode != 0:
106-
error_message = stderr.decode().strip()
107-
self.logger.log(f"ERRORS OCCURRED:\n{error_message}", 2)
156+
# Wait for both threads to complete
157+
stdout_thread.join()
158+
stderr_thread.join()
108159

109160
def run_topp(self, tool: str, input_output: dict, custom_params: dict = {}) -> None:
110161
"""

src/workflow/StreamlitUI.py

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -995,35 +995,67 @@ def execution_section(self, start_workflow_function) -> None:
995995
log_level = c1.selectbox(
996996
"log details", ["minimal", "commands and run times", "all"], key="log_level"
997997
)
998-
if self.executor.pid_dir.exists():
998+
999+
# Real-time display options
1000+
if "log_lines_count" not in st.session_state:
1001+
st.session_state.log_lines_count = 100
1002+
1003+
log_lines_count = c2.selectbox(
1004+
"lines to show", [50, 100, 200, 500, "all"],
1005+
index=1, key="log_lines_select"
1006+
)
1007+
if log_lines_count != "all":
1008+
st.session_state.log_lines_count = log_lines_count
1009+
1010+
pid_exists = self.executor.pid_dir.exists()
1011+
log_path = Path(self.workflow_dir, "logs", log_level.replace(" ", "-") + ".log")
1012+
log_exists = log_path.exists()
1013+
1014+
if pid_exists:
9991015
if c1.button("Stop Workflow", type="primary", use_container_width=True):
10001016
self.executor.stop()
10011017
st.rerun()
10021018
elif c1.button("Start Workflow", type="primary", use_container_width=True):
10031019
start_workflow_function()
1004-
st.rerun()
1005-
log_path = Path(self.workflow_dir, "logs", log_level.replace(" ", "-") + ".log")
1006-
if log_path.exists():
1007-
if self.executor.pid_dir.exists():
1008-
with st.spinner("**Workflow running...**"):
1009-
with open(log_path, "r", encoding="utf-8") as f:
1010-
st.code(
1011-
"".join(f.readlines()[-30:]),
1012-
language="neon",
1013-
line_numbers=False,
1014-
)
1015-
time.sleep(2)
1020+
with st.spinner("**Workflow running...**"):
1021+
time.sleep(1)
10161022
st.rerun()
1017-
else:
1018-
st.markdown(
1019-
f"**Workflow log file: {datetime.fromtimestamp(log_path.stat().st_ctime).strftime('%Y-%m-%d %H:%M')} CET**"
1020-
)
1023+
1024+
if log_exists and pid_exists:
1025+
# Real-time display during execution
1026+
with st.spinner("**Workflow running...**"):
10211027
with open(log_path, "r", encoding="utf-8") as f:
1022-
content = f.read()
1023-
# Check if workflow finished successfully
1024-
if not "WORKFLOW FINISHED" in content:
1025-
st.error("**Errors occurred, check log file.**")
1026-
st.code(content, language="neon", line_numbers=False)
1028+
lines = f.readlines()
1029+
if log_lines_count == "all":
1030+
display_lines = lines
1031+
else:
1032+
display_lines = lines[-st.session_state.log_lines_count:]
1033+
st.code(
1034+
"".join(display_lines),
1035+
language="neon",
1036+
line_numbers=False,
1037+
)
1038+
# Faster polling for real-time updates
1039+
time.sleep(1)
1040+
st.rerun()
1041+
1042+
elif log_exists and not pid_exists:
1043+
# Static display after completion
1044+
st.markdown(
1045+
f"**Workflow log file: {datetime.fromtimestamp(log_path.stat().st_ctime).strftime('%Y-%m-%d %H:%M')} CET**"
1046+
)
1047+
with open(log_path, "r", encoding="utf-8") as f:
1048+
content = f.read()
1049+
# Check if workflow finished successfully
1050+
if not "WORKFLOW FINISHED" in content:
1051+
st.error("**Errors occurred, check log file.**")
1052+
st.code(content, language="neon", line_numbers=False)
1053+
elif pid_exists:
1054+
with st.spinner("**Workflow running...**"):
1055+
time.sleep(1)
1056+
st.rerun()
1057+
1058+
10271059

10281060
def results_section(self, custom_results_function) -> None:
10291061
custom_results_function()

src/workflow/WorkflowManager.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ def start_workflow(self) -> None:
3434
# Add workflow process id to pid dir
3535
self.executor.pid_dir.mkdir()
3636
Path(self.executor.pid_dir, str(workflow_process.pid)).touch()
37-
time.sleep(3)
38-
st.rerun()
3937

4038
def workflow_process(self) -> None:
4139
"""

0 commit comments

Comments
 (0)