├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── setup.cfg ├── setup.py ├── streamlit_router └── __init__.py └── usage.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/__pycache__": true 4 | }, 5 | "python.linting.enabled": true, 6 | "python.linting.flake8Enabled": true, 7 | "python.linting.flake8Args": ["--max-line-length=240"], 8 | "editor.formatOnSave": true, 9 | "[python]": { 10 | "editor.defaultFormatter": "ms-python.black-formatter" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mapix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit-router 2 | 3 | ## Install 4 | 5 | ``` 6 | pip install streamlit-router 7 | ``` 8 | 9 | ![20240423141943](https://github.com/pragmatic-streamlit/streamlit-router/assets/932699/d59e78f3-7b1e-422a-be3b-6546a1040ca2) 10 | 11 | 12 | ## Example 13 | 14 | ``` 15 | import streamlit as st 16 | from streamlit_router import StreamlitRouter 17 | 18 | 19 | def index(router): 20 | st.text("fron page index") 21 | x = st.number_input("task id") 22 | if st.button("create task"): 23 | router.redirect(*router.build("create_task", {"x": x})) 24 | if st.button("cancel task"): 25 | router.redirect(*router.build("cancel_task", {"x": x})) 26 | if st.button("view task"): 27 | router.redirect(*router.build("view_task", {"x": x})) 28 | st.text("others on page index") 29 | 30 | 31 | # variable router auto inject if as first params 32 | def cancel_task(router, x): 33 | st.text(f"fron page cancel task x={x}") 34 | if st.button("back to index"): 35 | router.redirect(*router.build("index")) 36 | st.text("others on page cancel task") 37 | 38 | 39 | # variable router auto inject if as first params 40 | def create_task(x, router): 41 | st.text(f"fron page create task x={x}") 42 | if st.button("back to index"): 43 | router.redirect(*router.build("index")) 44 | st.text("others on page create task") 45 | 46 | 47 | router = StreamlitRouter() 48 | router.register(index, "/") 49 | router.register(cancel_task, "/tasks/", methods=["DELETE"]) 50 | router.register(create_task, "/tasks/", methods=["POST"]) 51 | 52 | 53 | # deco also works 54 | @router.map("/tasks/") 55 | def view_task(x): 56 | st.text(f"fron page view task x={x}") 57 | if st.button("back to index 2"): 58 | router.redirect(*router.build("index")) 59 | st.text("others on page view task") 60 | 61 | 62 | router.serve() 63 | 64 | ``` 65 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file=README.md 3 | license_files=LICENSE.rst -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="streamlit-router", 5 | version="0.1.8", 6 | author="mapix", 7 | author_email="mapix.me@gmail.com", 8 | packages=find_packages(exclude=["usage"]), 9 | install_requires=["streamlit", "werkzeug"], 10 | url="https://github.com/mapix/streamlit-router", 11 | description="werkzeug router for streamlit", 12 | long_description=open("README.md").read(), 13 | long_description_content_type="text/markdown", 14 | classifiers=[ 15 | "Intended Audience :: Developers", 16 | "Intended Audience :: Science/Research", 17 | "Programming Language :: Python :: 3", 18 | "Programming Language :: Python :: 3.6", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "License :: OSI Approved", 24 | "Topic :: Scientific/Engineering", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /streamlit_router/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import typing 3 | from uuid import uuid4 4 | from functools import wraps 5 | 6 | from werkzeug.routing import Map, Rule 7 | import streamlit as st 8 | 9 | 10 | class AttributeDict(dict): 11 | __getattr__ = dict.__getitem__ 12 | __setattr__ = dict.__setitem__ 13 | 14 | 15 | class StreamlitRouter: 16 | """for syntax @ 17 | https://werkzeug.palletsprojects.com/en/2.0.x/routing/ 18 | """ 19 | 20 | def __init__( 21 | self, 22 | default_path="/", 23 | inject_name="router", 24 | *, 25 | session_prefix="streamlit-router-prefix:", 26 | state_name="streamlit-router-state", 27 | endpoint_session_name="streamlit-router-endpoint", 28 | ): 29 | self._map = Map() 30 | self.view_methods = {} 31 | self.views = {} 32 | self.default_path = default_path 33 | self.inject_name = inject_name 34 | self.state_name = state_name 35 | self.session_prefix = session_prefix 36 | self.endpoint_session_name = endpoint_session_name 37 | self.urls = self._map.bind("", "/") 38 | if not st.session_state.get(self.state_name, None): 39 | setattr(st.session_state, self.state_name, AttributeDict()) 40 | 41 | def register( 42 | self, 43 | func: typing.Callable, 44 | path: str, 45 | methods: typing.List[str] = None, 46 | endpoint: str = None, 47 | ): 48 | return self.map(path, methods, endpoint)(func) 49 | 50 | def map( 51 | self, path: str, methods: typing.List[str] = None, endpoint: str = None 52 | ): # pylint: disable=unused-argument 53 | def _(func): 54 | nonlocal endpoint, methods 55 | if not endpoint: 56 | endpoint = func.__name__ 57 | if not methods: 58 | methods = ["GET"] 59 | self.views[endpoint] = func 60 | self._map.add(Rule(path, methods=methods, endpoint=endpoint)) 61 | self.view_methods[endpoint] = methods 62 | 63 | @wraps(func) 64 | def wraped(*args, **kwargs): 65 | if args: 66 | raise AssertionError( 67 | "positon style args not allowed for route func" 68 | ) 69 | argspec = inspect.getfullargspec(func).args 70 | if self.inject_name in argspec: 71 | kwargs[self.inject_name] = self 72 | return func(*args, **kwargs) 73 | 74 | return wraped 75 | 76 | return _ 77 | 78 | def handle(self, path: str, method: str = None): 79 | endpoint, kwargs = self.urls.match( 80 | path, method 81 | ) # pylint: disable=unpacking-non-sequence 82 | st.session_state[self.endpoint_session_name] = endpoint 83 | func = self.views[endpoint] 84 | argspec = inspect.getfullargspec(func).args 85 | if self.inject_name in argspec: 86 | kwargs[self.inject_name] = self 87 | return func(**kwargs) 88 | 89 | def redirect(self, path: str, method: str = None): 90 | self.reset_request_state() 91 | st.session_state["request"] = (path, method) 92 | st.session_state["request_id"] = uuid4().hex 93 | if hasattr(st, "rerun"): 94 | st.rerun() 95 | else: 96 | st.experimental_rerun() 97 | 98 | def get_request_id(self): 99 | if st.session_state.get("request_id", None) is None: 100 | st.session_state["request_id"] = uuid4().hex 101 | return st.session_state.get("request_id") 102 | 103 | def get_request_state(self, name: str = None, default=None): 104 | if st.session_state.get(self.state_name, None) is None: 105 | st.session_state[self.state_name] = AttributeDict() 106 | state = st.session_state.get(self.state_name) 107 | if name is None: 108 | return state 109 | if state.get(name, None) is None: 110 | state[name] = default 111 | return state.get(name) 112 | 113 | def delete_request_state(self, name: str): 114 | if st.session_state.get(self.state_name, None) is None: 115 | st.session_state[self.state_name] = AttributeDict() 116 | state = st.session_state.get(self.state_name) 117 | return state.pop(name, None) 118 | 119 | def set_request_state(self, name: str, value: typing.Any): 120 | if st.session_state.get(self.state_name, None) is None: 121 | st.session_state[self.state_name] = AttributeDict() 122 | state = st.session_state.get(self.state_name) 123 | state[name] = value 124 | 125 | def reset_request_state(self): 126 | if st.session_state.get(self.state_name, None) is not None: 127 | setattr(st.session_state, self.state_name, AttributeDict()) 128 | for k in st.session_state.keys(): 129 | if k.startswith(self.session_prefix): 130 | del st.session_state[k] 131 | 132 | def build(self, endpoint: str, values: typing.Dict = None, method: str = None): 133 | if not method and self.view_methods[endpoint]: 134 | method = self.view_methods[endpoint][0] 135 | return self.urls.build(endpoint, values), method 136 | 137 | def serve(self): 138 | request = st.session_state.get("request") 139 | query_dict = ( 140 | st.query_params 141 | if hasattr(st, "query_params") 142 | else st.experimental_get_query_params() 143 | ) 144 | if request: 145 | self.handle(*request) 146 | path, method = request 147 | query_dict["request"] = [f"{method}:{path}"] 148 | if not hasattr(st, "query_params"): 149 | st.experimental_set_query_params(**query_dict) 150 | elif "request" in query_dict: 151 | request = query_dict.get("request") 152 | if isinstance(request, str): 153 | method, path = request.split(":") 154 | elif isinstance(request, list): 155 | method, path = request[0].split(":") 156 | else: 157 | path = query_dict.get("request")[0] 158 | method = "GET" 159 | st.session_state["request"] = (path, method) 160 | if hasattr(st, "rerun"): 161 | st.rerun() 162 | else: 163 | st.experimental_rerun() 164 | else: 165 | self.handle(self.default_path) 166 | -------------------------------------------------------------------------------- /usage.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from streamlit_router import StreamlitRouter 3 | 4 | 5 | def index(router): 6 | st.text("fron page index") 7 | x = st.number_input("task id") 8 | if st.button("create task"): 9 | router.redirect(*router.build("create_task", {"x": x})) 10 | if st.button("cancel task"): 11 | router.redirect(*router.build("cancel_task", {"x": x})) 12 | if st.button("view task"): 13 | router.redirect(*router.build("view_task", {"x": x})) 14 | st.text("others on page index") 15 | 16 | 17 | # variable router auto inject if as first params 18 | def cancel_task(router, x): 19 | st.text(f"fron page cancel task x={x}") 20 | if st.button("back to index"): 21 | router.redirect(*router.build("index")) 22 | st.text("others on page cancel task") 23 | 24 | 25 | # variable router auto inject if as first params 26 | def create_task(x, router): 27 | st.text(f"fron page create task x={x}") 28 | if st.button("back to index"): 29 | router.redirect(*router.build("index")) 30 | st.text("others on page create task") 31 | 32 | 33 | router = StreamlitRouter() 34 | router.register(index, "/") 35 | router.register(cancel_task, "/tasks/", methods=["DELETE"]) 36 | router.register(create_task, "/tasks/", methods=["POST"]) 37 | 38 | 39 | # deco also works 40 | @router.map("/tasks/") 41 | def view_task(x): 42 | st.text(f"fron page view task x={x}") 43 | if st.button("back to index 2"): 44 | router.redirect(*router.build("index")) 45 | st.text("others on page view task") 46 | 47 | 48 | router.serve() 49 | --------------------------------------------------------------------------------