├── .gitignore ├── README.md ├── fastapi_wrapper_simple_demo.gif ├── lrp_app.py ├── lrp_bootstrapper.py ├── lrp_fastapi_wrapper.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | playground.ipynb 2 | .vscode 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Streamlit + FastAPI Integration 2 | A minimal Streamlit app showing how to launch and stop a FastAPI process on demand. The FastAPI `/run` route simulates a long-running process which is launched on a separate thread. 3 | 4 | Ensure the required packages are installed: 5 | 6 | ```bash 7 | pip install -r requirements.txt 8 | ``` 9 | 10 | To run the app: 11 | 12 | ```bash 13 | streamlit run lrp_app.py 14 | ``` 15 | 16 | ## Demo 17 | ![demo](./fastapi_wrapper_simple_demo.gif) -------------------------------------------------------------------------------- /fastapi_wrapper_simple_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asehmi/simple-streamlit-fastapi-integration/48037e9c22297eed523d2012c4d43ddca293cb7a/fastapi_wrapper_simple_demo.gif -------------------------------------------------------------------------------- /lrp_app.py: -------------------------------------------------------------------------------- 1 | import os, sys, json, base64 2 | import time 3 | import requests 4 | import streamlit as st 5 | import streamlit.components.v1 as components 6 | 7 | # -------------------------------------------------------------------------------- 8 | 9 | API_HOST='127.0.0.1' 10 | API_PORT=5000 11 | API_BASE_URL=f'http://{API_HOST}:{API_PORT}' 12 | 13 | # Session State variables: 14 | state = st.session_state 15 | if 'API_APP' not in state: 16 | state.API_APP = None 17 | if 'API_STARTED' not in state: 18 | state.API_STARTED=False 19 | 20 | # -------------------------------------------------------------------------------- 21 | 22 | # NOTE: Design point... only main() is allowed to mutate state. All supporting functions should not mutate state. 23 | def main(): 24 | st.title('Long Running Process Manager') 25 | 26 | # RUN LRP 27 | if not state.API_STARTED: 28 | st.write('To launch your LRP click the button below.') 29 | if st.button('🚀 Launch'): 30 | 31 | import subprocess 32 | import threading 33 | 34 | def run(job): 35 | print (f"\nRunning job: {job}\n") 36 | proc = subprocess.Popen(job) 37 | proc.wait() 38 | return proc 39 | 40 | job = [f'{sys.executable}', os.path.join('.', 'lrp_bootstrapper.py'), API_HOST, str(API_PORT)] 41 | 42 | # server thread will remain active as long as streamlit thread is running, or is manually shutdown 43 | thread = threading.Thread(name='FastAPI-LRP-Bootstrapper', target=run, args=(job,), daemon=True) 44 | thread.start() 45 | 46 | time.sleep(2) 47 | 48 | # !! Start the LRP !! 49 | requests.get(f'{API_BASE_URL}/run') 50 | 51 | state.API_STARTED = True 52 | 53 | st.experimental_rerun() 54 | 55 | if state.API_STARTED: 56 | message = {} 57 | c1, c2, _, c4 = st.columns([1,1,1,1]) 58 | with c1: 59 | if st.button('👋 Hello'): 60 | resp = requests.get(f'{API_BASE_URL}/hello') 61 | message = json.loads(resp.content) 62 | with c2: 63 | st.json(message) 64 | with c4: 65 | if st.button('🔥 Shutdown LRP'): 66 | requests.get(f'{API_BASE_URL}/shutdown') 67 | state.API_STARTED = False 68 | st.experimental_rerun() 69 | 70 | st.markdown(f''' 71 | #### Notes 72 | - `The long running process (LRP) and FastAPI is running.` 73 | - `To terminate the LRP, click the Shutdown button above.` 74 | - `To invoke the /hello endpoint, click the Hello button above.` 75 | #### API doc links 76 | `These FastAPI links only work in a localhost environment or if the FastAPI server 77 | is configured on an external domain reachable from this browser window!` 78 | - [**http://{API_HOST}:{API_PORT}/docs**](http://{API_HOST}:{API_PORT}/docs) 79 | - [**http://{API_HOST}:{API_PORT}/redoc**](http://{API_HOST}:{API_PORT}/redoc) 80 | ''') 81 | # st.markdown(''' 82 | # #### Embedded API docs 83 | # `Displays but works only in localhost environment!` 84 | # ''') 85 | # st.markdown('##### Swagger UI') 86 | # components.iframe(f'http://{API_HOST}:{API_PORT}/docs', height=600, scrolling=True) 87 | # st.markdown('##### Swagger Docs') 88 | # components.iframe(f'http://{API_HOST}:{API_PORT}/redoc', height=600, scrolling=True) 89 | 90 | def sidebar(): 91 | # ABOUT 92 | st.sidebar.header('About') 93 | st.sidebar.info('FastAPI Wrapper to run and stop a long running process (LRP)!\n\n' + \ 94 | '(c) 2023. CloudOpti Ltd. All rights reserved.') 95 | st.sidebar.markdown('---') 96 | 97 | 98 | if __name__ == '__main__': 99 | main() 100 | sidebar() 101 | -------------------------------------------------------------------------------- /lrp_bootstrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Simple bootstrapper intended to be used used to start the API as a daemon process 3 | """ 4 | import sys 5 | import uvicorn 6 | 7 | from lrp_fastapi_wrapper import FastAPI_Wrapper 8 | 9 | API_HOST='localhost' 10 | API_PORT=5000 11 | 12 | def stand_up(host=API_HOST, port=API_PORT): 13 | app = FastAPI_Wrapper() 14 | uvicorn.run(app, host=host, port=port) 15 | 16 | if __name__ == "__main__": 17 | stand_up(host=sys.argv[1], port=int(sys.argv[2])) 18 | -------------------------------------------------------------------------------- /lrp_fastapi_wrapper.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains the main `FastAPI_Wrapper` class, which wraps `FastAPI`. 3 | """ 4 | import os 5 | import time 6 | import datetime as dt 7 | import threading 8 | import psutil 9 | 10 | from fastapi import FastAPI 11 | from fastapi.responses import JSONResponse 12 | from fastapi.middleware.cors import CORSMiddleware 13 | 14 | CORS_ALLOW_ORIGINS=['http://localhost', 'http://localhost:5000', 'http://localhost:8765', 'http://127.0.0.1:5000'] 15 | 16 | class FastAPI_Wrapper(FastAPI): 17 | 18 | def __init__(self): 19 | """ 20 | Initializes a FastAPI instance to run a LRP. 21 | """ 22 | print('Initializing FastAPI_Wrapper...') 23 | 24 | super().__init__() 25 | 26 | origins = CORS_ALLOW_ORIGINS 27 | 28 | self.add_middleware( 29 | CORSMiddleware, 30 | allow_origins=origins, 31 | allow_credentials=True, 32 | allow_methods=["*"], 33 | allow_headers=["*"], 34 | ) 35 | 36 | # Add shutdown event (would only be of any use in a multi-process, not multi-thread situation) 37 | @self.get("/shutdown") 38 | async def shutdown(): 39 | 40 | def suicide(): 41 | time.sleep(1) 42 | myself = psutil.Process(os.getpid()) 43 | myself.kill() 44 | 45 | threading.Thread(target=suicide, daemon=True).start() 46 | print(f'>>> Successfully killed API <<<') 47 | return {"success": True} 48 | 49 | @self.get("/run") 50 | async def run(): 51 | # !! RUN YOUR LONG-RUNNING PROCESS HERE !! 52 | def lrp_runner(): 53 | while True: 54 | time.sleep(10) 55 | print(f'>>> LRP Report @ {dt.datetime.now()} <<<') 56 | 57 | threading.Thread(target=lrp_runner, daemon=True).start() 58 | 59 | @self.get("/hello") 60 | async def hello(): 61 | return JSONResponse({'message': 'Hello from FastAPI /hello endpoint!', 'time': dt.datetime.now().strftime('%y-%m-%d %H:%M:%S')}, status_code=200) 62 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit>=1.13.0 2 | uvicorn 3 | fastapi 4 | psutil 5 | --------------------------------------------------------------------------------