diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..7e6ee062 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" # Location of your pyproject.toml or requirements.txt + schedule: + interval: "weekly" # Checks for updates every week + commit-message: + prefix: "deps" # Prefix for pull request titles + open-pull-requests-limit: 5 # Limit the number of open PRs at a time diff --git a/.github/workflows/build-windows-executable-app.yaml b/.github/workflows/build-windows-executable-app.yaml index 04d0e2be..ddcbd8aa 100644 --- a/.github/workflows/build-windows-executable-app.yaml +++ b/.github/workflows/build-windows-executable-app.yaml @@ -14,7 +14,8 @@ env: OPENMS_VERSION: 3.2.0 PYTHON_VERSION: 3.11.0 # Name of the installer - APP_NAME: FLASHApp-0.70 + APP_NAME: FLASHApp-0.7.2 + APP_UpgradeCode: "69ae44ad-d554-4e3c-8715-7c4daf60f8bb" jobs: build-vue-js-component: @@ -58,6 +59,17 @@ jobs: repository: t0mdavid-m/OpenMS ref: FVdeploy path: 'OpenMS' + + # Temporary fix - until seqan is back online or new OpenMS release (3.4) + - name: Get latest cibuild.cmake + working-directory: OpenMS + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git fetch origin develop + git checkout origin/develop -- tools/ci/cibuild.cmake + git checkout origin/develop -- tools/ci/citest.cmake + git checkout origin/develop -- tools/ci/cipackage.cmake - name: Install Qt uses: jurplel/install-qt-action@v3 @@ -213,6 +225,13 @@ jobs: name: vue-js-dist path: js-dist + - name: Set Version in settings.json + run: | + $VERSION="${{ github.event.release.tag_name }}" + $content = Get-Content -Raw settings.json | ConvertFrom-Json + $content.version = $VERSION + $content | ConvertTo-Json -Depth 100 | Set-Content settings.json + - name: Download package as artifact uses: actions/download-artifact@v4 with: @@ -227,7 +246,7 @@ jobs: cp -r openms-package/share ../share - name: Set up Python (regular distribution) - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} # Use the same version as the embeddable version @@ -276,7 +295,15 @@ jobs: - name: Create .bat file run: | - echo " start /min .\python-${{ env.PYTHON_VERSION }}\python -m streamlit run app.py local" > ${{ env.APP_NAME }}.bat + echo '@echo off' > ${{ env.APP_NAME }}.bat + echo '' >> ${{ env.APP_NAME }}.bat + echo 'REM Create .streamlit directory in user''s home if it doesn''t exist' >> ${{ env.APP_NAME }}.bat + echo 'if not exist "%USERPROFILE%\.streamlit" mkdir "%USERPROFILE%\.streamlit"' >> ${{ env.APP_NAME }}.bat + echo '' >> ${{ env.APP_NAME }}.bat + echo 'REM Create credentials.toml with empty email to disable email prompt' >> ${{ env.APP_NAME }}.bat + echo 'copy /Y ".streamlit\credentials.toml" "%USERPROFILE%\.streamlit\credentials.toml" > nul' >> ${{ env.APP_NAME }}.bat + echo '' >> ${{ env.APP_NAME }}.bat + echo 'start /min .\python-${{ env.PYTHON_VERSION }}\python -m streamlit run app.py local' >> ${{ env.APP_NAME }}.bat - name: Create All-in-one executable folder run: | @@ -358,7 +385,7 @@ jobs: cat < streamlit_exe.wxs - + diff --git a/.github/workflows/test-win-exe-w-embed-py.yaml b/.github/workflows/test-win-exe-w-embed-py.yaml index 30fd3eb5..3af5f523 100644 --- a/.github/workflows/test-win-exe-w-embed-py.yaml +++ b/.github/workflows/test-win-exe-w-embed-py.yaml @@ -10,6 +10,7 @@ jobs: env: PYTHON_VERSION: 3.11.9 + APP_UpgradeCode: 4abc2e23-3ba5-40e4-95c9-09e6cb8ecaeb APP_NAME: OpenMS-StreamlitTemplateApp-Test steps: @@ -115,7 +116,7 @@ jobs: cat < streamlit_exe.wxs - + diff --git a/.gitignore b/.gitignore index baf5a6b3..9934b497 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ myenv build.log run_app.spec clean-up-workspaces.log +gdpr_consent/node_modules/ # FLASHApp workspaces @@ -23,4 +24,7 @@ workspaces-flashtaggerviewer # Temp files **/__pycache__ .DS_Store +*-embed-amd64 +*~ + diff --git a/.streamlit/config.toml b/.streamlit/config.toml index e0cf8889..a68c7cb8 100644 --- a/.streamlit/config.toml +++ b/.streamlit/config.toml @@ -1,12 +1,14 @@ +[browser] +gatherUsageStats = false + [global] developmentMode = false [server] +maxUploadSize = 2000 #MB port = 8501 # should be same as configured in deployment repo -maxUploadSize = 2000 [theme] - # The preset Streamlit theme that your custom theme inherits from. One of "light" or "dark". # base = @@ -23,4 +25,4 @@ primaryColor = "#29379b" # textColor = # Font family for all text in the app, except code blocks. One of "sans serif", "serif", or "monospace". -# font = \ No newline at end of file +# font = diff --git a/.streamlit/credentials.toml b/.streamlit/credentials.toml new file mode 100644 index 00000000..63eee49f --- /dev/null +++ b/.streamlit/credentials.toml @@ -0,0 +1,2 @@ +[general] +email = "" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7bea3162..5f5caebc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,8 +73,7 @@ RUN wget -q \ RUN mamba --version # Setup mamba environment. -COPY environment.yml ./environment.yml -RUN mamba env create -f environment.yml +RUN mamba create -n streamlit-env python=3.10 RUN echo "mamba activate streamlit-env" >> ~/.bashrc SHELL ["/bin/bash", "--rcfile", "~/.bashrc"] SHELL ["mamba", "run", "-n", "streamlit-env", "/bin/bash", "-c"] @@ -115,6 +114,10 @@ RUN make -j4 pyopenms WORKDIR /openms-build/pyOpenMS RUN pip install dist/*.whl +# Install other dependencies (excluding pyopenms) +COPY requirements.txt ./requirements.txt +RUN grep -v '^pyopenms' requirements.txt > requirements_cleaned.txt && mv requirements_cleaned.txt requirements.txt +RUN pip install -r requirements.txt WORKDIR / RUN mkdir openms @@ -172,14 +175,15 @@ RUN mamba run -n streamlit-env python hooks/hook-analytics.py # Set Online Deployment RUN jq '.online_deployment = true' settings.json > tmp.json && mv tmp.json settings.json -# Download latest OpenMS App executable for Windows from Github actions workflow. +# Download latest OpenMS App executable as a ZIP file RUN if [ -n "$GH_TOKEN" ]; then \ echo "GH_TOKEN is set, proceeding to download the release asset..."; \ - gh release download --repo ${GITHUB_USER}/${GITHUB_REPO} --pattern "${ASSET_NAME}" --dir /app; \ + gh release download -R ${GITHUB_USER}/${GITHUB_REPO} -p "OpenMS-App.zip" -D /app; \ else \ echo "GH_TOKEN is not set, skipping the release asset download."; \ fi + # Run app as container entrypoint. EXPOSE $PORT ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/app.py b/app.py index 46b43607..c19c1e36 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,13 @@ import streamlit as st from pathlib import Path +import json # For some reason the windows version only works if this is imported here import pyopenms +if "settings" not in st.session_state: + with open("settings.json", "r") as f: + st.session_state.settings = json.load(f) + if __name__ == '__main__': pages = { "FLASHApp" : [ diff --git a/assets/openms_transparent_bg_logo.svg b/assets/openms_transparent_bg_logo.svg new file mode 100644 index 00000000..b4149ede --- /dev/null +++ b/assets/openms_transparent_bg_logo.svg @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..450b16e1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.8' + +services: + openms-streamlit-template: + build: + context: . + dockerfile: Dockerfile + args: + GITHUB_TOKEN: $GITHUB_TOKEN + image: openms_streamlit_template + container_name: openms-streamlit-template + restart: always + ports: + - 8501:8501 + volumes: + - workspaces-streamlit-template:/workspaces-streamlit-template + command: streamlit run openms-streamlit-template/app.py +volumes: + workspaces-streamlit-template: diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 5cfc4eeb..00000000 --- a/environment.yml +++ /dev/null @@ -1,19 +0,0 @@ -###### TODO: how use exact version specifiers for all packages ###### -name: streamlit-env -channels: -- conda-forge -dependencies: -- python==3.11 -- plotly==5.22.0 -- pip==24.0 -- numpy==1.26.4 # pandas and numpy are dependencies of pyopenms, however, pyopenms needs numpy<=1.26.4 -- mono==6.12.0.90 -- pyarrow<16 -- scipy>=1.15 -- pip: - # dependencies only available through pip - # streamlit dependencies - - streamlit==1.39.0 - - captcha==0.5.0 - - pyopenms_viz>=0.1.2 - - streamlit-js-eval diff --git a/requirements.txt b/requirements.txt index c909fca0..ddeedfcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,12 @@ # the requirements.txt file is intended for deployment on streamlit cloud and if the simple container is built # note that it is much more restricted in terms of installing third-parties / etc. # preferably use the batteries included or simple docker file for local hosting -streamlit==1.39.0 +streamlit==1.43.0 numpy==1.26.4 # pandas and numpy are dependencies of pyopenms, however, pyopenms needs numpy<=1.26.4 plotly==5.22.0 -captcha==0.5.0 +captcha==0.7.1 pyopenms>=3.2.0 pyarrow<16 streamlit-js-eval scipy>=1.15 +psutil diff --git a/run_app.py b/run_app.py index b736b1bb..0b6e18f4 100644 --- a/run_app.py +++ b/run_app.py @@ -1,7 +1,8 @@ from streamlit.web import cli + if __name__ == "__main__": cli._main_run_clExplicit( - file="app.py", command_line="streamlit run" + file="app.py", + command_line="streamlit run" ) - # we will create this function inside our streamlit framework diff --git a/settings.json b/settings.json index 508f8882..8db60611 100644 --- a/settings.json +++ b/settings.json @@ -1,6 +1,7 @@ { "app-name": "FLASHApp", "github-user": "OpenMS", + "version": "0.7.3", "repository-name": "FLASHApp", "analytics": { "google-analytics": { @@ -12,5 +13,8 @@ "tag": "57690c44-d635-43b0-ab43-f8bd3064ca06" } }, - "online_deployment": false + "online_deployment": false, + "enable_workspaces": true, + "test": true, + "workspaces_dir": ".." } \ No newline at end of file diff --git a/src/common/captcha_.py b/src/common/captcha_.py index b8dfaa68..288eed03 100644 --- a/src/common/captcha_.py +++ b/src/common/captcha_.py @@ -234,6 +234,7 @@ def captcha_control(): data = image.generate(st.session_state["Captcha"]) st.image(data) c1, c2 = st.columns([70, 30]) + capta2_text = st.empty() capta2_text = c1.text_input("Enter captcha text", max_chars=5) c2.markdown("##") if c2.form_submit_button("Verify the code", type="primary"): diff --git a/src/common/common.py b/src/common/common.py index f26c2602..249734e9 100644 --- a/src/common/common.py +++ b/src/common/common.py @@ -10,6 +10,7 @@ import streamlit as st import pandas as pd +import psutil try: from tkinter import Tk, filedialog @@ -24,6 +25,20 @@ OS_PLATFORM = sys.platform +@st.fragment(run_every=5) +def monitor_hardware(): + cpu_progress = psutil.cpu_percent(interval=None) / 100 + ram_progress = 1 - psutil.virtual_memory().available / psutil.virtual_memory().total + + st.text(f"Ram ({ram_progress * 100:.2f}%)") + st.progress(ram_progress) + + st.text(f"CPU ({cpu_progress * 100:.2f}%)") + st.progress(cpu_progress) + + st.caption(f"Last fetched at: {time.strftime('%H:%M:%S')}") + + def load_params(default: bool = False) -> dict[str, Any]: """ Load parameters from a JSON file and return a dictionary containing them. @@ -41,6 +56,11 @@ def load_params(default: bool = False) -> dict[str, Any]: Returns: dict[str, Any]: A dictionary containing the parameters. """ + + # Check if workspace is enabled. If not, load default parameters. + if not st.session_state.settings["enable_workspaces"]: + default = True + # Construct the path to the parameter file path = Path(st.session_state.workspace, "params.json") @@ -75,6 +95,11 @@ def save_params(params: dict[str, Any]) -> None: Returns: dict[str, Any]: Updated parameters. """ + + # Check if the workspace is enabled and if a 'params.json' file exists in the workspace directory + if not st.session_state.settings["enable_workspaces"]: + return + # Update the parameter dictionary with any modified parameters from the current session for key, value in st.session_state.items(): if key in params.keys(): @@ -107,7 +132,7 @@ def page_setup(page: str = "") -> dict[str, Any]: # Set Streamlit page configurations st.set_page_config( page_title=st.session_state.settings["app-name"], - page_icon="assets/OpenMS.png", + page_icon="assets/openms_transparent_bg_logo.svg", layout="wide", initial_sidebar_state="auto", menu_items=None, @@ -131,9 +156,9 @@ def page_setup(page: str = "") -> dict[str, Any]: # Create google analytics if consent was given if ( - ("tracking_consent" not in st.session_state) + ("tracking_consent" not in st.session_state) or (st.session_state.tracking_consent is None) - or (not st.session_state.settings['online_deployment']) + or (not st.session_state.settings["online_deployment"]) ): st.session_state.tracking_consent = None else: @@ -198,22 +223,44 @@ def page_setup(page: str = "") -> dict[str, Any]: if "windows" in sys.argv: os.chdir("../streamlit-template") # Define the directory where all workspaces will be stored - workspaces_dir = Path("..", "workspaces-" + st.session_state.settings["repository-name"]) - if "workspace" in st.query_params: - st.session_state.workspace = Path(workspaces_dir, st.query_params.workspace) - elif st.session_state.location == "online": - workspace_id = str(uuid.uuid1()) - st.session_state.workspace = Path(workspaces_dir, workspace_id) - st.query_params.workspace = workspace_id + if ( + st.session_state.settings["workspaces_dir"] + and st.session_state.location == "local" + ): + workspaces_dir = Path( + st.session_state.settings["workspaces_dir"], + "workspaces-" + st.session_state.settings["repository-name"], + ) + else: + workspaces_dir = ".." + + # Check if workspace logic is enabled + if st.session_state.settings["enable_workspaces"]: + if "workspace" in st.query_params: + st.session_state.workspace = Path( + workspaces_dir, st.query_params.workspace + ) + elif st.session_state.location == "online": + workspace_id = str(uuid.uuid1()) + st.session_state.workspace = Path(workspaces_dir, workspace_id) + st.query_params.workspace = workspace_id + else: + st.session_state.workspace = Path(workspaces_dir, "default") + st.query_params.workspace = "default" + else: + # Use default workspace when workspace feature is disabled st.session_state.workspace = Path(workspaces_dir, "default") - st.query_params.workspace = "default" if st.session_state.location != "online": # not any captcha so, controllo should be true st.session_state["controllo"] = True - if "workspace" not in st.query_params: + # If no workspace is specified and workspace feature is enabled, set default workspace and query param + if ( + "workspace" not in st.query_params + and st.session_state.settings["enable_workspaces"] + ): st.query_params.workspace = st.session_state.workspace.name # Make sure the necessary directories exist @@ -222,8 +269,21 @@ def page_setup(page: str = "") -> dict[str, Any]: # Render the sidebar params = render_sidebar(page) - - captcha_control() + + captcha_control() + + # If run in hosted mode, show captcha as long as it has not been solved + # if not "local" in sys.argv: + # if "controllo" not in st.session_state: + # # Apply captcha by calling the captcha_control function + # captcha_control() + + # If run in hosted mode, show captcha as long as it has not been solved + if "controllo" not in st.session_state or ( + "controllo" in params.keys() and params["controllo"] == False + ): + # Apply captcha by calling the captcha_control function + captcha_control() return params @@ -248,52 +308,93 @@ def render_sidebar(page: str = "") -> None: params = load_params() with st.sidebar: # The main page has workspace switcher - with st.expander("🖥️ **Workspaces**"): - # Define workspaces directory outside of repository - workspaces_dir = Path("..", "workspaces-" + st.session_state.settings["repository-name"]) - # Online: show current workspace name in info text and option to change to other existing workspace - if st.session_state.location == "local": - # Define callback function to change workspace - def change_workspace(): - for key in params.keys(): - if key in st.session_state.keys(): - del st.session_state[key] - st.session_state.workspace = Path( - workspaces_dir, st.session_state["chosen-workspace"] + # Display workspace switcher if workspace is enabled in local mode + if st.session_state.settings["enable_workspaces"]: + with st.expander("🖥️ **Workspaces**"): + # Workspaces directory specified in the settings.json + if ( + st.session_state.settings["workspaces_dir"] + and st.session_state.location == "local" + ): + workspaces_dir = Path( + st.session_state.settings["workspaces_dir"], + "workspaces-" + st.session_state.settings["repository-name"], ) - st.query_params.workspace = st.session_state["chosen-workspace"] - - # Get all available workspaces as options - options = [ - file.name for file in workspaces_dir.iterdir() if file.is_dir() - ] - # Let user chose an already existing workspace - st.selectbox( - "choose existing workspace", - options, - index=options.index(str(st.session_state.workspace.stem)), - on_change=change_workspace, - key="chosen-workspace", - ) - # Create or Remove workspaces - create_remove = st.text_input("create/remove workspace", "") - path = Path(workspaces_dir, create_remove) - # Create new workspace - if st.button("**Create Workspace**"): - path.mkdir(parents=True, exist_ok=True) - st.session_state.workspace = path - st.query_params.workspace = create_remove - # Temporary as the query update takes a short amount of time - time.sleep(1) - st.rerun() - # Remove existing workspace and fall back to default - if st.button("⚠️ Delete Workspace"): - if path.exists(): - shutil.rmtree(path) - st.session_state.workspace = Path(workspaces_dir, "default") - st.query_params.workspace = "default" - st.rerun() - + else: + workspaces_dir = ".." + # Online: show current workspace name in info text and option to change to other existing workspace + if st.session_state.location == "local": + # Define callback function to change workspace + def change_workspace(): + for key in params.keys(): + if key in st.session_state.keys(): + del st.session_state[key] + st.session_state.workspace = Path( + workspaces_dir, st.session_state["chosen-workspace"] + ) + st.query_params.workspace = st.session_state["chosen-workspace"] + + # Get all available workspaces as options + options = [ + file.name for file in workspaces_dir.iterdir() if file.is_dir() + ] + # Let user chose an already existing workspace + st.selectbox( + "choose existing workspace", + options, + index=options.index(str(st.session_state.workspace.stem)), + on_change=change_workspace, + key="chosen-workspace", + ) + # Create or Remove workspaces + create_remove = st.text_input("create/remove workspace", "").strip() + path = Path(workspaces_dir, create_remove) + # Create new workspace + if st.button("**Create Workspace**"): + if create_remove: + path.mkdir(parents=True, exist_ok=True) + st.session_state.workspace = path + st.query_params.workspace = create_remove + # Temporary as the query update takes a short amount of time + time.sleep(1) + st.rerun() + else: + st.warning("Please enter a valid workspace name.") + # Remove existing workspace and fall back to default + if st.button("⚠️ Delete Workspace"): + if path.exists(): + shutil.rmtree(path) + st.session_state.workspace = Path(workspaces_dir, "default") + st.query_params.workspace = "default" + st.rerun() + + with st.expander("📊 **Resource Utilization**"): + monitor_hardware() + + # Display OpenMS WebApp Template Version from settings.json + with st.container(): + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + version_info = st.session_state.settings["version"] + app_name = st.session_state.settings["app-name"] + st.markdown( + f'
{app_name}
Version: {version_info}
', + unsafe_allow_html=True, + ) return params @@ -359,12 +460,16 @@ def get_current_chunk(df, chunk_size, chunk_index): ) rows = event["selection"]["rows"] - if not rows: - return None - # Calculate the index based on the current page and chunk size - base_index = (page - 1) * chunk_size - return base_index + rows[0] + if st.session_state.settings["test"]: # is a test App, return first row as selected + return 1 + elif not rows: + return None + else: + # Calculate the index based on the current page and chunk size + base_index = (page - 1) * chunk_size + print(base_index) + return base_index + rows[0] def show_table(df: pd.DataFrame, download_name: str = "") -> None: diff --git a/src/workflow/ParameterManager.py b/src/workflow/ParameterManager.py index 20b90ef7..4348da7c 100644 --- a/src/workflow/ParameterManager.py +++ b/src/workflow/ParameterManager.py @@ -77,7 +77,7 @@ def save_parameters(self) -> None: with open(self.params_file, "w", encoding="utf-8") as f: json.dump(json_params, f, indent=4) - def get_parameters_from_json(self) -> None: + def get_parameters_from_json(self) -> dict: """ Loads parameters from the JSON file if it exists and returns them as a dictionary. If the file does not exist, it returns an empty dictionary.