Package streamlit app and run executable on windows

Question:

this is my first question on Stackoverflow. I hope my question is clear, otherwise let me know and don’t hesitate to ask me more details.

I’m trying to package a streamlit app for a personal project. I’m developing under linux but I have to deploy the app on Windows. I want it to be a standalone executable, which once run opens the browser tab to display the app, and exits when the tab is closed. I would like to use pynsist library to package the app (already used for another project and it worked fine).

I followed the suggestion found in this discussion. It worked fine on ubuntu, and apparently also on Windows after packaging the app with pynsist. "Apparently" because the executable run, but no browser tab was open to display the app.

Here is some snippets of my code.

Project structure

|- installer.cfg
|- src
    |- main.py
    |- run_app.py

main.py

import streamlit as st

st.title("Test")
st.title("My first app deployed with Pynsist!")

run_app.py
(EDIT 2 after comment by Thomas K)

import os
import subprocess
import sys

from src.config import EnvironmentalVariableNames as EnvVar, get_env

def main():
    executable = sys.executable
    result = subprocess.run(
        f"{executable} -m streamlit run {os.path.join(get_env(EnvVar.EMPORIO_VESTIARIO_DASHBOARD_WORKING_DIR), 'src', 'main.py')}",
        shell=True,
        capture_output=True,
        text=True,
    )


if __name__ == "__main__":
    main()

EMPORIO_VESTIARIO_DASHBOARD_WORKING_DIR is an environmental variable to make the app work on both linux and windows (on windows, it is set to the installation directory).

pynsist installer.cfg

EDIT: including dependencies of streamlit discovered through pip list

EDIT 2: added MarkupSafe as dependency of Jinja2

[Application]
name=Emporio Vestiario Dashboard
version=0.1.0
# How to lunch the app - this calls the 'main' function from the 'myapp' package:
entry_point=src.run_app:main
icon=resources/caritas-logo.ico

[Python]
version=3.8.10
bitness=64

[Include]
# Packages from PyPI that your application requires, one per line
# These must have wheels on PyPI:
pypi_wheels = altair==4.1.0
    astor==0.8.1
    attrs==21.2.0
    backcall==0.2.0
    backports.zoneinfo==0.2.1
    base58==2.1.0
    bleach==4.1.0
    blinker==1.4
    cachetools==4.2.2
    certifi==2021.5.30
    cffi==1.14.6
    charset-normalizer==2.0.6
    click==7.1.2
    decorator==5.1.0
    defusedxml==0.7.1
    distlib==0.3.3
    entrypoints==0.3
    idna==3.2
    jsonschema==3.2.0
    mistune==0.8.4
    mypy-extensions==0.4.3
    numpy==1.21.1
    packaging==21.0
    pandas==1.3.3
    pandocfilters==1.5.0
    parso==0.8.2
    pillow==8.3.2
    platformdirs==2.4.0
    prompt-toolkit==3.0.20
    protobuf==3.18.0
    pyarrow==5.0.0
    pycparser==2.20
    pydeck==0.7.0
    pyparsing==2.4.7
    pyrsistent==0.18.0
    python-dateutil==2.8.2
    pytz==2021.1
    requests==2.26.0
    requests-download==0.1.2
    send2trash==1.8.0
    setuptools==57.0.0
    six==1.14.0
    smmap==4.0.0
    streamlit==0.89.0
    terminado==0.12.1
    testpath==0.5.0
    toml==0.10.2
    tomli==1.2.1
    toolz==0.11.1
    tornado==6.1
    traitlets==5.1.0
    typing-extensions==3.10.0.2
    tzlocal==3.0
    urllib3==1.26.7
    validators==0.18.2
    Jinja2==3.0.1
    MarkupSafe==2.0.1

Looking at the executable output on Windows, the current working directory is correctly printed, but no other output (streamlit app initialization message, or error messages) is printed. I tried to open the browser and go to localhost:8501, but I got connection error.

Any hints on how to make the code execute and automatically open the browser tab? Any help is greatly appreciated!

EDIT: as pointed out in the comment to the last package in installer.cfg, the app (with Jinja2 dependency) is correctly installed on windows, but when launched, the app still cannot find Jinja2 dependency. This is the traceback:

Traceback (most recent call last):
  File "Emporio_Vestiario_Dashboard.launch.pyw", line 34, in <module>
    from src.run_app import main
  File "C:Userstantardinidevelopcaritaspkgssrcrun_app.py", line 6, in <module>
    import streamlit
  File "C:Userstantardinidevelopcaritaspkgsstreamlit__init__.py", line 75, in <module>
    from streamlit.delta_generator import DeltaGenerator as _DeltaGenerator
  File "C:Userstantardinidevelopcaritaspkgsstreamlitdelta_generator.py", line 70, in <module>
    from streamlit.elements.arrow import ArrowMixin
  File "C:Userstantardinidevelopcaritaspkgsstreamlitelementsarrow.py", line 20, in <module>
    from pandas.io.formats.style import Styler
  File "C:Userstantardinidevelopcaritaspkgspandasioformatsstyle.py", line 49, in <module>
    jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.")
  File "C:Userstantardinidevelopcaritaspkgspandascompat_optional.py", line 118, in import_optional_dependency
    raise ImportError(msg) from None
ImportError: Missing optional dependency 'Jinja2'. DataFrame.style requires jinja2. Use pip or conda to install Jinja2.

EDIT 2: thanks to the helpful hints by Thomas K, I came up with half a solution. The app runs and streamlit is started.

But.

These are the log messages:

  Welcome to Streamlit!

  If you're one of our development partners or you're interested in getting
  personal technical support or Streamlit updates, please enter your email
  address below. Otherwise, you may leave the field blank.

  Email:
2021-10-11 20:56:53.202 WARNING streamlit.config:
Warning: the config option 'server.enableCORS=false' is not compatible with 'server.enableXsrfProtection=true'.
As a result, 'server.enableCORS' is being overridden to 'true'.

More information:
In order to protect against CSRF attacks, we send a cookie with each request.
To do so, we must specify allowable origins, which places a restriction on
cross-origin resource sharing.

If cross origin resource sharing is required, please disable server.enableXsrfProtection.
           
2021-10-11 20:56:53.202 DEBUG   streamlit.logger: Initialized tornado logs
2021-10-11 20:56:53.202 ERROR   streamlit.credentials: 

It seems that the execution of the app is stopped becuase it is waiting for some credentials. I found here that a .streamlit/credentials.toml can be added, but I’m not sure on the exact location on windows. I’ve also tried to explicitly add --server.headless=false in the subprocess.run command, but again with no effect.

Why the app doesn’t start automatically like on Linux? Is there a way to start the app without additional configurations by the user?

Asked By: mattiatantardini

||

Answers:

EDIT: a streamlit example was added to the examples of pynsist repo. Here you can find a minimal and refined example of a working application (which also includes plotly).

ORIGINAL ANSWER

Finally I get it to work. In my last attempt, I made a mistake by setting --server.headless=false, while it must be true instead. I found that an additional flag to the streamlit run command is needed: --global.developmentMode=false. This make the deploy work, even if I could not find any reference to this configuration in the streamlit configurations.

Working code follows.

Project structure

|- wheels/
|- installer.cfg
|- src
    |- main.py
    |- run_app.py

main.py

import streamlit as st

st.title("Test")
st.title("My first app deployed with Pynsist!")

run_app.py

import os
import subprocess
import sys
import webbrowser

from src.config import EnvironmentalVariableNames as EnvVar, get_env


def main():

    # Getting path to python executable (full path of deployed python on Windows)
    executable = sys.executable

    # Open browser tab. May temporarily display error until streamlit server is started.
    webbrowser.open("http://localhost:8501")

    # Run streamlit server
    path_to_main = os.path.join(
        get_env(EnvVar.EMPORIO_VESTIARIO_DASHBOARD_WORKING_DIR), "src", "app.py"
    )
    result = subprocess.run(
        f"{executable} -m streamlit run {path_to_main} --server.headless=true --global.developmentMode=false",
        shell=True,
        capture_output=True,
        text=True,
    )

    # These are printed only when server is stopped.
    # NOTE: you have to manually stop streamlit server killing process.
    print(result.stdout)
    print(result.stderr)


if __name__ == "__main__":
    main()

Some notes:

  1. webbrowser.open is necessary to automatically open a new tab in the browser to show the streamlit app. The subprocess.run lines only starts a new streamlit server.
  2. As I pointed out in the comments, once exiting the streamlit tab in the browser, the streamlit server is still there and active. You may access again the dashboard by only typing localhost:8501 in the address bar. If you click multiple times on the Windows app icon, multiple streamlit servers are started. I’ve tried with only two active at the same time, and they do not show conflicting behaviour. To stop them you have to manually end tasks through task manager, for instance.

installer.cfg

[Application]
name=Emporio Vestiario Dashboard
version=0.1.0
# How to lunch the app - this calls the 'main' function from the 'myapp' package:
entry_point=src.run_app:main
icon=resources/caritas-logo.ico

[Python]
version=3.8.10
bitness=64

[Include]
# Packages from PyPI that your application requires, one per line
# These must have wheels on PyPI:
pypi_wheels = altair==4.1.0
    astor==0.8.1
    attrs==21.2.0
    backcall==0.2.0
    backports.zoneinfo==0.2.1
    base58==2.1.0
    bleach==4.1.0
    blinker==1.4
    cachetools==4.2.2
    certifi==2021.5.30
    cffi==1.14.6
    charset-normalizer==2.0.6
    click==7.1.2
    decorator==5.1.0
    defusedxml==0.7.1
    distlib==0.3.3
    entrypoints==0.3
    idna==3.2
    jsonschema==3.2.0
    mistune==0.8.4
    mypy-extensions==0.4.3
    numpy==1.21.1
    packaging==21.0
    pandas==1.3.3
    pandocfilters==1.5.0
    parso==0.8.2
    pillow==8.3.2
    platformdirs==2.4.0
    prompt-toolkit==3.0.20
    protobuf==3.18.0
    pyarrow==5.0.0
    pycparser==2.20
    pydeck==0.7.0
    pyparsing==2.4.7
    pyrsistent==0.18.0
    python-dateutil==2.8.2
    pytz==2021.1
    requests==2.26.0
    requests-download==0.1.2
    send2trash==1.8.0
    setuptools==57.0.0
    six==1.14.0
    smmap==4.0.0
    streamlit==0.89.0
    terminado==0.12.1
    testpath==0.5.0
    toml==0.10.2
    tomli==1.2.1
    toolz==0.11.1
    tornado==6.1
    traitlets==5.1.0
    typing-extensions==3.10.0.2
    tzlocal==3.0
    urllib3==1.26.7
    validators==0.18.2
    Jinja2==3.0.1
    MarkupSafe==2.0.1

extra_wheel_sources = ./wheels

Note: blinker extra wheels is required.

EDIT 2

As @ananvodo pointed out, it may not be immediately clear what EnvVar and get_env are. I copy here the relevant parts of config.py for sake of completeness.

import os


class EnvironmentalVariableNames:
    """Defines the names of the environmental variables used in the code and useful shortcuts"""

    # Environmental variables for Zaccheo app
    EMPORIO_VESTIARIO_DASHBOARD_WORKING_DIR= "EMPORIO_VESTIARIO_DASHBOARD_WORKING_DIR"

    # Other environmental variable names defined hereafter


def get_env(env_var):
    """Returns the value of the environment variable env_var"""
    return os.environ[env_var]
Answered By: mattiatantardini
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.