31 |
32 |
112 | {% endblock %}
113 |
--------------------------------------------------------------------------------
/docs/api/modules.rst:
--------------------------------------------------------------------------------
1 | streamlitextras
2 | ===============
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | streamlitextras
8 |
--------------------------------------------------------------------------------
/docs/api/streamlitextras.authenticator.rst:
--------------------------------------------------------------------------------
1 | streamlitextras.authenticator package
2 | =====================================
3 |
4 | .. automodule:: streamlitextras.authenticator
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 | Submodules
10 | ----------
11 |
12 | streamlitextras.authenticator.exceptions module
13 | -----------------------------------------------
14 |
15 | .. automodule:: streamlitextras.authenticator.exceptions
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
20 | streamlitextras.authenticator.user module
21 | -----------------------------------------
22 |
23 | .. automodule:: streamlitextras.authenticator.user
24 | :members:
25 | :undoc-members:
26 | :show-inheritance:
27 |
28 | streamlitextras.authenticator.utils module
29 | ------------------------------------------
30 |
31 | .. automodule:: streamlitextras.authenticator.utils
32 | :members:
33 | :undoc-members:
34 | :show-inheritance:
35 |
--------------------------------------------------------------------------------
/docs/api/streamlitextras.cookiemanager.rst:
--------------------------------------------------------------------------------
1 | streamlitextras.cookiemanager package
2 | =====================================
3 |
4 | .. automodule:: streamlitextras.cookiemanager
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/api/streamlitextras.logger.rst:
--------------------------------------------------------------------------------
1 | streamlitextras.logger package
2 | ==============================
3 |
4 | .. automodule:: streamlitextras.logger
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 | Submodules
10 | ----------
11 |
12 | streamlitextras.logger.firebasesink module
13 | ------------------------------------------
14 |
15 | .. automodule:: streamlitextras.logger.firebasesink
16 | :members:
17 | :undoc-members:
18 | :show-inheritance:
19 |
--------------------------------------------------------------------------------
/docs/api/streamlitextras.router.rst:
--------------------------------------------------------------------------------
1 | streamlitextras.router package
2 | ==============================
3 |
4 | .. automodule:: streamlitextras.router
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/api/streamlitextras.rst:
--------------------------------------------------------------------------------
1 | streamlitextras package
2 | =======================
3 |
4 | .. automodule:: streamlitextras
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
9 | Subpackages
10 | -----------
11 |
12 | .. toctree::
13 | :maxdepth: 4
14 |
15 | streamlitextras.authenticator
16 | streamlitextras.cookiemanager
17 | streamlitextras.logger
18 | streamlitextras.router
19 | streamlitextras.threader
20 |
21 | Submodules
22 | ----------
23 |
24 | streamlitextras.helpers module
25 | ------------------------------
26 |
27 | .. automodule:: streamlitextras.helpers
28 | :members:
29 | :undoc-members:
30 | :show-inheritance:
31 |
32 | streamlitextras.storageservice module
33 | -------------------------------------
34 |
35 | .. automodule:: streamlitextras.storageservice
36 | :members:
37 | :undoc-members:
38 | :show-inheritance:
39 |
40 | streamlitextras.utils module
41 | ----------------------------
42 |
43 | .. automodule:: streamlitextras.utils
44 | :members:
45 | :undoc-members:
46 | :show-inheritance:
47 |
48 | streamlitextras.webutils module
49 | -------------------------------
50 |
51 | .. automodule:: streamlitextras.webutils
52 | :members:
53 | :undoc-members:
54 | :show-inheritance:
55 |
--------------------------------------------------------------------------------
/docs/api/streamlitextras.threader.rst:
--------------------------------------------------------------------------------
1 | streamlitextras.threader package
2 | ================================
3 |
4 | .. automodule:: streamlitextras.threader
5 | :members:
6 | :undoc-members:
7 | :show-inheritance:
8 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/master/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 |
15 | import os
16 | import sys
17 |
18 | sys.path.insert(0, os.path.abspath(".."))
19 | sys.path.insert(0, os.path.abspath("_extensions"))
20 | sys.path.insert(0, os.path.abspath(".streamlit"))
21 |
22 |
23 | # -- Project information -----------------------------------------------------
24 |
25 | project = "StreamlitExtras"
26 | copyright = "2022, A.D."
27 | author = "BLIPK"
28 |
29 | # The short X.Y version
30 | version = ""
31 | # The full version, including alpha/beta/rc tags
32 | release = ""
33 |
34 |
35 | # -- General configuration ---------------------------------------------------
36 |
37 | # If your documentation needs a minimal Sphinx version, state it here.
38 | #
39 | # needs_sphinx = '1.0'
40 |
41 | # Add any Sphinx extension module names here, as strings. They can be
42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
43 | # ones.
44 | extensions = [
45 | "sphinx.ext.autodoc",
46 | "sphinx.ext.napoleon",
47 | "sphinx.ext.viewcode",
48 | "sphinx.ext.intersphinx",
49 | "sphinxcontrib.apidoc",
50 | "autodoc_stub_file",
51 | ]
52 |
53 | apidoc_module_dir = "../streamlitextras"
54 | apidoc_output_dir = "./api"
55 | apidoc_module_first = True
56 |
57 | # Add any paths that contain templates here, relative to this directory.
58 | templates_path = ["_templates"]
59 |
60 | # The suffix(es) of source filenames.
61 | # You can specify multiple suffix as a list of string:
62 | #
63 | # source_suffix = ['.rst', '.md']
64 | source_suffix = ".rst"
65 |
66 | # The master toctree document.
67 | master_doc = "index"
68 |
69 | # The language for content autogenerated by Sphinx. Refer to documentation
70 | # for a list of supported languages.
71 | #
72 | # This is also used if you do content translation via gettext catalogs.
73 | # Usually you set "language" from the command line for these cases.
74 | language = "English"
75 |
76 | # List of patterns, relative to source directory, that match files and
77 | # directories to ignore when looking for source files.
78 | # This pattern also affects html_static_path and html_extra_path .
79 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
80 |
81 | # The name of the Pygments (syntax highlighting) style to use.
82 | pygments_style = "sphinx"
83 |
84 | # Warn about all references where the target cannot be found.
85 | nitpicky = True
86 |
87 |
88 | # -- Options for HTML output -------------------------------------------------
89 |
90 | # The theme to use for HTML and HTML Help pages. See the documentation for
91 | # a list of builtin themes.
92 | #
93 | html_theme = "sphinx_rtd_theme"
94 |
95 | # Theme options are theme-specific and customize the look and feel of a theme
96 | # further. For a list of options available for each theme, see the
97 | # documentation.
98 | #
99 | html_theme_options = {}
100 |
101 | # Add any paths that contain custom static files (such as style sheets) here,
102 | # relative to this directory. They are copied after the builtin static files,
103 | # so a file named "default.css" will overwrite the builtin "default.css".
104 | html_static_path = ["_static"]
105 |
106 | # Custom sidebar templates, must be a dictionary that maps document names
107 | # to template names.
108 | #
109 | # The default sidebars (for documents that don't match any pattern) are
110 | # defined by theme itself. Builtin themes are using these templates by
111 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
112 | # 'searchbox.html']``.
113 | #
114 | # html_sidebars = {}
115 |
116 |
117 | # -- Options for HTMLHelp output ---------------------------------------------
118 |
119 | # Output file base name for HTML help builder.
120 | htmlhelp_basename = "streamlitextrasdoc"
121 |
122 |
123 | # -- Options for LaTeX output ------------------------------------------------
124 |
125 | latex_elements = {
126 | # The paper size ('letterpaper' or 'a4paper').
127 | #
128 | # 'papersize': 'letterpaper',
129 | # The font size ('10pt', '11pt' or '12pt').
130 | #
131 | # 'pointsize': '10pt',
132 | # Additional stuff for the LaTeX preamble.
133 | #
134 | # 'preamble': '',
135 | # Latex figure (float) alignment
136 | #
137 | # 'figure_align': 'htbp',
138 | }
139 |
140 | # Grouping the document tree into LaTeX files. List of tuples
141 | # (source start file, target name, title,
142 | # author, documentclass [howto, manual, or own class]).
143 | latex_documents = [
144 | (
145 | master_doc,
146 | "streamlitextras.tex",
147 | "Streamlit Extras Documentation",
148 | "BLIPK",
149 | "manual",
150 | )
151 | ]
152 |
153 |
154 | # -- Options for manual page output ------------------------------------------
155 |
156 | # One entry per manual page. List of tuples
157 | # (source start file, name, description, authors, manual section).
158 | man_pages = [
159 | (master_doc, "streamlitextras", "Streamlit Extras Documentation", [author], 1)
160 | ]
161 |
162 |
163 | # -- Options for Texinfo output ----------------------------------------------
164 |
165 | # Grouping the document tree into Texinfo files. List of tuples
166 | # (source start file, target name, title, author,
167 | # dir menu entry, description, category)
168 | texinfo_documents = [
169 | (
170 | master_doc,
171 | "streamlitextras",
172 | "Streamlit Extras Documentation",
173 | author,
174 | "streamlitextras",
175 | "One line description of project.",
176 | "Miscellaneous",
177 | )
178 | ]
179 |
180 |
181 | # -- Extension configuration -------------------------------------------------
182 |
183 | html_context = {"github_user": "blipk", "github_repo": "streamlitextras"}
184 |
185 | add_module_names = False
186 | autodoc_member_order = "bysource"
187 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
188 | html_show_sourcelink = False
189 | html_show_copyright = False
190 | napoleon_use_rtype = False
191 | napoleon_use_ivar = True
192 |
193 |
194 | def setup(app):
195 | # app.add_css_file("css/mod.css")
196 | app.add_js_file("js/copybutton.js")
197 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../README.rst
2 |
3 | Table of contents
4 | =================
5 |
6 | .. toctree::
7 | :includehidden:
8 | :maxdepth: 2
9 |
10 | overview.rst
11 | api/streamlitextras.rst
12 | project.rst
13 |
--------------------------------------------------------------------------------
/docs/overview.rst:
--------------------------------------------------------------------------------
1 | Overview
2 | ========
3 |
4 | .. include:: ../README.rst
5 |
--------------------------------------------------------------------------------
/docs/project.rst:
--------------------------------------------------------------------------------
1 | Project Information
2 | ===================
3 |
4 | .. toctree::
5 |
6 | project/license.rst
7 |
--------------------------------------------------------------------------------
/docs/project/license.rst:
--------------------------------------------------------------------------------
1 | License
2 | #######
3 |
4 | .. include:: ../../LICENSE
5 |
--------------------------------------------------------------------------------
/docs/reruntrigger.py:
--------------------------------------------------------------------------------
1 | timestamp = 1666657170.2776253
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | streamlit>=1.23.1
2 | urllib3==1.26.16 # Newer versions break pyrebase
3 | requests
4 | gcloud
5 | pyjwt
6 | firebase
7 | pyrebase4
8 | sseclient
9 | PyCryptodome
10 | requests_toolbelt
11 | firebase-admin
12 | google-cloud-storage
13 |
14 | twine
15 | Sphinx==5.2.3 ; python_version>='3.10'
16 | sphinx-autobuild==2021.3.14 ; python_version>='3.10'
17 | sphinx-rtd-theme==1.0.0 ; python_version>='3.10'
18 | docutils==0.16 ; python_version>='3.10'
19 | sphinxcontrib-apidoc ; python_version>='3.10'
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | package_name = "streamlit-base-extras"
4 | version_init_file = "streamlitextras/__init__.py"
5 |
6 | try:
7 | from setuptools import setup, find_packages
8 | except ImportError:
9 | from distutils.core import setup, find_packages
10 |
11 |
12 | with open(version_init_file, "r") as file:
13 | regex_version = r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]'
14 | version = re.search(regex_version, file.read(), re.MULTILINE).group(1)
15 |
16 | with open("README.md", "r") as f:
17 | readme = f.read()
18 |
19 | setup(
20 | name=package_name,
21 | version=version,
22 | author="blipk",
23 | author_email="blipk+@github.com",
24 | description="Make building with streamlit easier.",
25 | long_description=readme,
26 | long_description_content_type="text/markdown",
27 | url="https://github.com/blipk/streamlitextras",
28 | packages=find_packages(),
29 | include_package_data=True,
30 | download_url=f"https://github.com/blipk/streamlitextras/archive/{version}.tar.gz",
31 | project_urls={
32 | "Changelog": "https://github.com/blipk/streamlitextras/commits/",
33 | "Documentation": "https://streamlitextras.readthedocs.io/en/stable/index.html",
34 | },
35 | keywords=[
36 | "streamlitextras",
37 | "streamlit",
38 | "router",
39 | "authenticator",
40 | "javascript",
41 | "cookie",
42 | "thread",
43 | ],
44 | license="MIT license",
45 | classifiers=[
46 | "Development Status :: 5 - Production/Stable",
47 | "License :: OSI Approved :: MIT License",
48 | "Operating System :: OS Independent",
49 | "Intended Audience :: Developers",
50 | "Natural Language :: English",
51 | "Topic :: Internet :: WWW/HTTP",
52 | "Topic :: Internet :: WWW/HTTP :: Browsers",
53 | "Topic :: Software Development :: Libraries",
54 | "Programming Language :: Python",
55 | "Programming Language :: Python :: 3",
56 | "Programming Language :: Python :: 3.10",
57 | "Programming Language :: Python :: 3 :: Only",
58 | "Programming Language :: Python :: Implementation :: PyPy",
59 | "Programming Language :: Python :: Implementation :: CPython",
60 | ],
61 | install_requires=[
62 | "streamlit >= 1.23.1",
63 | "streamlit-javascript",
64 | "loguru",
65 | "requests",
66 | "gcloud",
67 | "pyjwt",
68 | "firebase",
69 | "pyrebase4",
70 | "sseclient",
71 | "PyCryptodome",
72 | "requests_toolbelt",
73 | "firebase-admin",
74 | "google-cloud-storage",
75 | ],
76 | extras_require={
77 | "dev": [
78 | # Docs
79 | "Sphinx==5.2.3 ; python_version>='3.10'",
80 | "sphinx-autobuild==2021.3.14 ; python_version>='3.10'",
81 | "sphinx-rtd-theme==1.0.0 ; python_version>='3.10'",
82 | "docutils==0.16 ; python_version>='3.10'",
83 | "sphinxcontrib-apidoc ; python_version>='3.10'",
84 | ]
85 | },
86 | python_requires=">=3.10",
87 | )
88 |
--------------------------------------------------------------------------------
/streamlitextras/__init__.py:
--------------------------------------------------------------------------------
1 | # from .router import Router
2 | # from .cookiemanager import CookieManager
3 | # from .authenticator import Authenticator
4 | # from .authenticator.user import User
5 | # from .authenticator.exceptions import AuthException, LoginError, RegisterError, ResetError, UpdateError
6 |
7 | __version__ = "0.2.45"
8 |
--------------------------------------------------------------------------------
/streamlitextras/authenticator/README.md:
--------------------------------------------------------------------------------
1 | # Streamlit Extras Authenticator
2 |
3 | Authenticator is a class to manage creating streamlit authentication forms (login/registration) and to create and manage users via firebase auth.
4 |
5 | #### Basic usage
6 |
7 | ```Python
8 | import streamlit as st
9 | from streamlitextras.authenticator import get_auth
10 |
11 | auth = None
12 | def main():
13 | global auth
14 | auth = get_auth("my_cookie_name")
15 | auth.delayed_init() # This is required to make sure current Authenticator stays in session state
16 |
17 | auth_status = auth.auth_status
18 | user = auth.current_user
19 |
20 | if auth_status and user:
21 | st.write(f"Welcome {user.displayName}!")
22 | else:
23 | auth_page()
24 |
25 | def auth_page():
26 | info_box = st.container()
27 |
28 | if auth.current_form == "login" or not auth.current_form:
29 | user, res, error = auth.login("Login")
30 | if auth.current_form == "register":
31 | res, error = auth.register_user("Register")
32 | elif auth.current_form == "reset_password":
33 | res, error = auth.reset_password("Request password change email")
34 |
35 | if res:
36 | info_box.info("Success!")
37 |
38 | if error:
39 | info_box.error(error.message)
40 |
41 | if __name__ == "__main__":
42 | main()
43 |
44 | ```
45 |
46 | You will also need to set up some secrets in .streamlit/secrets.toml, you can find the firebase details through the section at the bottom on your firebase project settings page.
47 |
48 | ```TOML
49 | [authenticator]
50 | cookie_key = "my_c00k!e_jwt_k3y"
51 | admin_ids = ""
52 | developer_ids = ""
53 |
54 | [firebase]
55 | apiKey = ""
56 | authDomain = ""
57 | projectId = ""
58 | storageBucket = ""
59 | messagingSenderId = ""
60 | appId = ""
61 | measurementId = ""
62 | databaseURL = ""
63 | ```
64 |
65 | ###### The User
66 |
67 | As per the example above, when a user is logged in you can access it with `auth.current_user`. The user class has two basic properties `is_admin` and `is_developer` which return True if the users firebase localId is in your config.toml admin_ids or developer_ids
68 |
69 | It also has the function `User.refresh_token()` which will update the users firebase auth refresh token, this is done automatically in Authenticator when required, but should also be used before making any additional calls to firebase or google cloud that use the users `localId`
70 |
71 | You will also probably want extend the User class to implement user functionality specific to your app, you can provide a reference to your own class that inherits `authenticator.User` and pass it to `get_auth("cookie_name", user_class=MyUser)`
72 |
73 | ```Python
74 | from streamlitextras.authenticator import User
75 |
76 | class MyUser(User):
77 | def __init__(self, authenticator, **kwargs):
78 | super().__init__(authenticator, **kwargs)
79 |
80 | def get_user_files():
81 | """Get user files or something else specific to your app"""
82 | ```
83 |
84 | #### Advanced usage
85 |
86 | If you want to use the streamlit integration but use a different authentication service provider, you can create your own class that inherits Authenticator and override the private methods.
--------------------------------------------------------------------------------
/streamlitextras/authenticator/exceptions.py:
--------------------------------------------------------------------------------
1 | # Exceptions for the firebase auth helper in autenticator.py
2 | from typing import Union, Optional
3 | from requests import HTTPError
4 |
5 |
6 | class AuthException(Exception):
7 | """
8 | Base exception for Authenticator class
9 | """
10 |
11 | def __init__(
12 | self,
13 | message: str,
14 | firebase_error: Optional[str] = None,
15 | requests_exception: Optional[Union[HTTPError, Exception]] = None,
16 | ):
17 | """
18 | :param str message: message to be displayed to the user
19 | :param Optional[str] firebase_error: firebase error message enum e.g INVALID_PASSWORD
20 | :param Union[HTTPError, Exception] requests_exception: requests.HTTPError with .response and .request attributes (or other associated exception)
21 | """
22 | self.message = message
23 | self.firebase_error = firebase_error
24 | self.requests_exception = requests_exception
25 |
26 |
27 | class LoginError(AuthException):
28 | """Exceptions raised for the login user widget."""
29 |
30 |
31 | class RegisterError(AuthException):
32 | """Exceptions raised for the register user widget."""
33 |
34 |
35 | class ResetError(AuthException):
36 | """Exceptions raised for the rest password widget."""
37 |
38 |
39 | class UpdateError(AuthException):
40 | """Exceptions raised for the update user details widget."""
41 |
--------------------------------------------------------------------------------
/streamlitextras/authenticator/user.py:
--------------------------------------------------------------------------------
1 | from streamlitextras.logger import log
2 | from streamlitextras.utils import repr_
3 | from streamlitextras.authenticator.utils import handle_firebase_action
4 | from streamlitextras.authenticator.exceptions import (
5 | AuthException,
6 | LoginError,
7 | RegisterError,
8 | ResetError,
9 | UpdateError,
10 | )
11 |
12 |
13 | class User:
14 | """
15 | This class is used as an interface for Authenticators users
16 | """
17 |
18 | def __init__(self, authenticator, auth_data, login_data={}, debug: bool = False):
19 | """
20 | Initializes a user account with associated firebase tokens and account information
21 |
22 | :param Authenticator authenticator: The associated Authenticator class that spawned this user
23 | :param dict auth_data:
24 | Dict containing session_cookie, decoded_claims and user_record
25 | :param dict login_data:
26 | The data from the initial login, not always provided (when reading session cookies)
27 | """
28 | self.debug = debug
29 |
30 | self.authenticator = authenticator
31 | self.auth_data = auth_data
32 | self.login_data = login_data
33 |
34 | self.idToken = login_data.get("idToken", None)
35 | self.expiresIn = login_data.get("expiresIn", None)
36 | self.registered = login_data.get("registered", None)
37 | self.refreshToken = login_data.get("refreshToken", None)
38 |
39 | self.user_record = auth_data["user_record"]
40 | self.session_cookie = auth_data["session_cookie"]
41 | self.decoded_claims = auth_data["decoded_claims"]
42 |
43 | self.uid = self.decoded_claims["uid"]
44 | self.localId = self.user_record["localId"]
45 |
46 | self.email = self.user_record["email"]
47 | self.emailVerified = self.user_record["emailVerified"]
48 | self.displayName = self.user_record.get("displayName", None)
49 | self.createdAt = self.user_record.get("createdAt", None)
50 | self.lastLoginAt = self.user_record.get("lastLoginAt", None)
51 | self.lastRefreshAt = self.user_record.get("lastRefreshAt", None)
52 | self.passwordUpdatedAt = self.user_record.get("passwordUpdatedAt", None)
53 | self.providerUserInfo = self.user_record.get("providerUserInfo", None)
54 | self.validSince = self.user_record.get("validSince", None)
55 | self.photoUrl = self.user_record.get("photoUrl", None)
56 |
57 | self.account_info = login_data.get("account_info", None)
58 | self.users = self.account_info["users"] if self.account_info else None
59 | self.user = self.users[0] if self.users else None
60 | self.disabled = self.user.get("disabled", None) if self.user else None
61 | self.customAuth = self.user.get("customAuth", None) if self.user else None
62 |
63 | def refresh_token(self):
64 | refreshed = None
65 | refresh_errors = {
66 | "TOKEN_EXPIRED": "Too many recent sessions. Please try again later."
67 | }
68 | user_refresh, refresh_error = handle_firebase_action(
69 | self.authenticator.auth.refresh,
70 | LoginError,
71 | refresh_errors,
72 | self.refreshToken,
73 | )
74 | if not refresh_error:
75 | self.login_data = {**self.login_data, **user_refresh}
76 | for key in self.__dict__.keys():
77 | if key in user_refresh and hasattr(self, key):
78 | setattr(self, key, user_refresh[key])
79 | self.authenticator._create_session(self.login_data)
80 | refreshed = True
81 | if self.debug:
82 | log.info(f"Users tokens refreshed {self.uid}.")
83 | else:
84 | log.error(f"Error refreshing users firebase token {refresh_error}")
85 | refreshed = False
86 |
87 | return refreshed
88 |
89 | @property
90 | def firebase_user(self):
91 | """
92 | Gets the UserRecord object from the official firebase python SDK
93 |
94 | # https://firebase.google.com/docs/reference/admin/python/firebase_admin.auth#firebase_admin.auth.UserRecord
95 | """
96 | self.authenticator.service_auth.get_user(self.uid)
97 |
98 | @property
99 | def is_admin(self):
100 | """
101 | Returns true if users firebase id is in self.authenticator.admin_ids
102 | """
103 | admin_ids = self.authenticator.admin_ids
104 | return self.localId in admin_ids
105 |
106 | @property
107 | def is_developer(self):
108 | """
109 | Returns true if users firebase id is in self.authenticator.developer_ids
110 | """
111 | developer_ids = self.authenticator.developer_ids
112 | return self.localId in developer_ids
113 |
114 | def __repr__(self) -> str:
115 | return repr_(
116 | self,
117 | ["passwordHash", "login_data", "refreshToken", "idToken", "authenticator"],
118 | only_keys=["uid", "email"],
119 | )
120 |
--------------------------------------------------------------------------------
/streamlitextras/authenticator/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import requests
3 |
4 |
5 | # Proxy for handling pyrebase/firebase errors
6 | def handle_firebase_action(
7 | action_function, exception_type, error_dict, *fn_args, **fn_kwargs
8 | ):
9 | """
10 | Handler for pyrebase/firebase actions.
11 | """
12 | error = None
13 | result = None
14 | try:
15 | # print("Args", fn_args, fn_kwargs)
16 | result = action_function(*fn_args, **fn_kwargs)
17 | except requests.HTTPError as e:
18 | # Pyrebase/4 is intercepting and raising an incorrectly initialized requests.HTTPError,
19 | # so the response has to be extracted from builtin BaseException args rather than e.response
20 | response_dict = json.loads(e.args[0].response.text.replace('"', '"'))
21 | error_type = None
22 | try:
23 | error_type = response_dict["error"]["message"]
24 | except:
25 | try:
26 | error_type = response_dict["error"]["errors"][0]["message"]
27 | except:
28 | pass
29 |
30 | if e.args[0].response.status_code == 400:
31 | if not error_dict:
32 | error = exception_type(
33 | f"Unknown {exception_type.__name__} while trying to {action_function.__name__} ({error_type})",
34 | error_type,
35 | e.args[0],
36 | )
37 | else:
38 | if error_type in error_dict:
39 | error = exception_type(
40 | error_dict[error_type], error_type, e.args[0]
41 | )
42 | else:
43 | for key in error_dict:
44 | if key in error_type:
45 | message = error_dict[key] + " " + error_type.split(" : ")[1]
46 | error = exception_type(message, error_type, e.args[0])
47 | if not error:
48 | error = exception_type(
49 | f"Unknown {exception_type.__name__} while trying to {action_function.__name__} ({error_type})",
50 | error_type,
51 | e.args[0],
52 | )
53 | else:
54 | error = exception_type(
55 | f"Storage network error, please try again later ({e.args[0].response.status_code}) ({error_type})",
56 | error_type,
57 | e.args[0],
58 | )
59 | print(e.args[0])
60 | if not error:
61 | raise
62 | except requests.ConnectionError as e:
63 | error = exception_type(
64 | "Storage connection error, please try again later", f"SERVER_{e.args[0]}", e
65 | )
66 |
67 | return (result, error)
68 |
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/README.md:
--------------------------------------------------------------------------------
1 | # Streamlit Extras Cookie Manager
2 |
3 | Cookie Manager is a streamlit component function that uses the npm library universal-cookie to manage browser cookies from your streamlit code.
4 |
5 |
6 | #### Basic usage
7 |
8 | ```Python
9 | import streamlit as st
10 | from streamlitextras.cookiemanager import get_cookie_manager
11 |
12 | cookie_manager = None
13 | def main():
14 | global cookie_manager
15 | cookie_manager = get_cookie_manager()
16 | cookie_manager.delayed_init() # Makes sure CookieManager stays in st.session_state
17 |
18 | cookie_manager.set("my_cookie_name", "I'm a cookie!")
19 | my_cookie_value = cookie_manager.get("my_cookie_name")
20 | print(my_cookie_value) # "I'm a cookie"
21 |
22 | my_cookies = cookie_manager.get_all()
23 | print(my_cookies) # {"my_cookie_name": "I'm a cookie!"}
24 |
25 | cookie_manager.delete("my_cookie_name")
26 | my_cookie_value = cookie_manager.get("my_cookie_name")
27 | print(my_cookie_value) # None
28 |
29 |
30 | if __name__ == "__main__":
31 | main()
32 | ```
33 |
34 | The cookie will default to expire in 720 minutes in the users browsers timezone.
35 |
36 |
37 | #### Advanced usage
38 |
39 | Pythons `CookieManager.set()` also supports setting most cookie flags/properties e.g.
40 |
41 | ```Python
42 | import pytz
43 | import streamlit as st
44 | from streamlitextras.webutils import get_user_timezone
45 | from streamlitextras.cookiemanager import get_cookie_manager
46 |
47 | user_tz = get_user_timezone()
48 | expiry_date = (datetime.now(pytz.UTC) + timedelta(seconds=60*60))
49 | cookie_expiry_time = expiry_date.astimezone(tz=pytz.timezone(user_tz))
50 |
51 | cookie_manager = get_cookie_manager()
52 | cookie_manager.set("my_cookie_name", "I expire in one hour", expires_at=cookie_expiry_time)
53 | ```
54 |
55 | For a full list see the [API docs](https://streamlitextras.readthedocs.io/en/latest/api/streamlitextras.html).
56 |
57 | ###### Note
58 |
59 | CookieManager will add a reference to the javascript universal-cookie class to both the JS globals `parent` and `window` object, so you can access it from the browser console or other parts of your app as `window.CookieManager`.
60 |
61 | Some streamlit componenets run in iframes so you may need to use `parent.window.CookieManager`
62 |
63 | Also note that this is a different CookieManager class to the Python CookieManager.
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import streamlit as st
4 | import streamlit.components.v1 as components
5 | from streamlitextras.logger import log
6 | from streamlitextras.utils import repr_
7 |
8 | absolute_path = os.path.dirname(os.path.abspath(__file__))
9 | build_path = os.path.join(absolute_path, "frontend/build")
10 | _component_func = components.declare_component("cookie_manager", path=build_path)
11 |
12 |
13 | class CookieManager:
14 | """
15 | This is a streamlit component class to manage cookies.
16 |
17 | It uses a thin component instance wrapper around the universal-cookie library.
18 | """
19 |
20 | def __init__(self, debug: bool = False):
21 | if "cookie_manager" in st.session_state and st.session_state["cookie_manager"]:
22 | self.cookies = st.session_state["cookie_manager"].cookies.copy()
23 | else:
24 | self.cookies = {}
25 | st.session_state["cookie_manager"] = self
26 | self.debug = debug
27 | if self.debug:
28 | log.debug(f"Initialized Cookie Manager {hex(id(self))}")
29 |
30 | def delayed_init(self):
31 | """
32 | Used to delay initialization of streamlit objects so this class can be cached
33 | """
34 | st.session_state["cookie_manager"] = self
35 |
36 | def cookie_manager(self, *args, **kwargs):
37 | time.sleep(0.1)
38 | try:
39 | result = _component_func(**kwargs)
40 | except st.errors.DuplicateWidgetID:
41 | kwargs["key"] = f"""{kwargs["key"]}{time.time()}"""
42 | result = _component_func(**kwargs)
43 | time.sleep(0.1) # This ensures we get a result before streamlit redraws
44 | return result
45 |
46 | def set(
47 | self,
48 | name,
49 | value,
50 | expires_at=None,
51 | secure=None,
52 | path=None,
53 | same_site=None,
54 | key="set",
55 | ):
56 | """
57 | Set a cookie with name, value and options.
58 | Defaults are set in the JS component and listed below
59 |
60 | :param name: The name of the cookie
61 | :param value: The value of the cookie
62 | :param expires_at:
63 | Datetime of when the cookie expires.
64 | Default is set in the js at 1 day from browser timezone.
65 | :param secure: Secure flag, default is true.
66 | :param path: Cookie path, default is /
67 | :param same_site: Same site attribute, default is "strict"
68 |
69 | :param key: streamlit key used for the component instance
70 |
71 | :returns: True if the operation was successful, else False
72 | """
73 | if name is None or name == "":
74 | return False
75 |
76 | expires_at = expires_at.isoformat() if expires_at else None
77 | options = {
78 | "name": name,
79 | "value": value,
80 | "expires": expires_at,
81 | "sameSite": same_site,
82 | }
83 | result = self.cookie_manager(
84 | method="set", options=options, key=key, default=False
85 | )
86 | if result:
87 | self.cookies[name] = result
88 |
89 | return True
90 |
91 | def get(self, name, key="get"):
92 | """
93 | Gets a value of a cookie.
94 | NOTE: The value isn't got directly from the component instance
95 | This class fills all cookies in self.cookies everytime streamlit instances it on the page
96 |
97 | Returns empty {} dict if no result. Returns None if the operation failed.
98 |
99 | :param name: The name of the cookie to get the value of
100 |
101 | :returns Union[str, dict]:
102 | Returns the cookie value, it may be deserialized from JSON to a dict
103 | """
104 | if not self.cookies:
105 | self.cookies = self.cookie_manager(method="getAll", key=key, default={})
106 |
107 | if name is None or name == "":
108 | return None
109 | result = self.cookies.get(name, None)
110 | time.sleep(0.4) # Give component time to render
111 | return result
112 |
113 | def get_all(self, key="get_all"):
114 | """
115 | Get a dict of all the cookies in the browser, and update self.cookies
116 |
117 | :param key: streamlit key used for the component instance
118 |
119 | :returns: A dict of all the cookies
120 | """
121 | self.cookies = self.cookie_manager(method="getAll", key=key, default={})
122 | return self.cookies
123 |
124 | def delete(self, name, key="delete"):
125 | """
126 | Delete a cookie from the browser
127 |
128 | :param name: the name of the cookie to delete
129 | :param key: streamlit key used for the component instance
130 |
131 | :returns: True if the operation was successful, or else False
132 | """
133 | if name is None or name == "":
134 | return False
135 |
136 | result = self.cookie_manager(
137 | method="delete", options={"name": name}, key=key, default=False
138 | )
139 | if result and name in self.cookies:
140 | del self.cookies[name]
141 | return result
142 |
143 | def __repr__(self) -> str:
144 | return repr_(self)
145 |
146 |
147 | # @st.cache(allow_output_mutation=True, show_spinner=False)
148 | def get_cookie_manager() -> CookieManager:
149 | if "cookie_manager" in st.session_state and st.session_state["cookie_manager"]:
150 | return st.session_state["cookie_manager"]
151 | return CookieManager()
152 |
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/asset-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": {
3 | "main.js": "./static/js/main.a8b332f3.chunk.js",
4 | "main.js.map": "./static/js/main.a8b332f3.chunk.js.map",
5 | "runtime-main.js": "./static/js/runtime-main.41b280b5.js",
6 | "runtime-main.js.map": "./static/js/runtime-main.41b280b5.js.map",
7 | "static/js/2.2bc12ddb.chunk.js": "./static/js/2.2bc12ddb.chunk.js",
8 | "static/js/2.2bc12ddb.chunk.js.map": "./static/js/2.2bc12ddb.chunk.js.map",
9 | "index.html": "./index.html",
10 | "precache-manifest.d091de9fa4668477ba52acdbecc2c864.js": "./precache-manifest.d091de9fa4668477ba52acdbecc2c864.js",
11 | "service-worker.js": "./service-worker.js",
12 | "static/js/2.2bc12ddb.chunk.js.LICENSE.txt": "./static/js/2.2bc12ddb.chunk.js.LICENSE.txt"
13 | },
14 | "entrypoints": [
15 | "static/js/runtime-main.41b280b5.js",
16 | "static/js/2.2bc12ddb.chunk.js",
17 | "static/js/main.a8b332f3.chunk.js"
18 | ]
19 | }
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/index.html:
--------------------------------------------------------------------------------
1 | Cookie Manager
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/precache-manifest.d091de9fa4668477ba52acdbecc2c864.js:
--------------------------------------------------------------------------------
1 | self.__precacheManifest = (self.__precacheManifest || []).concat([
2 | {
3 | "revision": "d52117df20953279ee158609f127fa64",
4 | "url": "./index.html"
5 | },
6 | {
7 | "revision": "af08ea6b6607896320f4",
8 | "url": "./static/js/2.2bc12ddb.chunk.js"
9 | },
10 | {
11 | "revision": "065af7f54f052065d2d7d684b53f2ae2",
12 | "url": "./static/js/2.2bc12ddb.chunk.js.LICENSE.txt"
13 | },
14 | {
15 | "revision": "0e04dd2981e94467c748",
16 | "url": "./static/js/main.a8b332f3.chunk.js"
17 | },
18 | {
19 | "revision": "58b1ca89039dae1abe7e",
20 | "url": "./static/js/runtime-main.41b280b5.js"
21 | }
22 | ]);
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/service-worker.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Welcome to your Workbox-powered service worker!
3 | *
4 | * You'll need to register this file in your web app and you should
5 | * disable HTTP caching for this file too.
6 | * See https://goo.gl/nhQhGp
7 | *
8 | * The rest of the code is auto-generated. Please don't update this file
9 | * directly; instead, make changes to your Workbox build configuration
10 | * and re-run your build process.
11 | * See https://goo.gl/2aRDsh
12 | */
13 |
14 | importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
15 |
16 | importScripts(
17 | "./precache-manifest.d091de9fa4668477ba52acdbecc2c864.js"
18 | );
19 |
20 | self.addEventListener('message', (event) => {
21 | if (event.data && event.data.type === 'SKIP_WAITING') {
22 | self.skipWaiting();
23 | }
24 | });
25 |
26 | workbox.core.clientsClaim();
27 |
28 | /**
29 | * The workboxSW.precacheAndRoute() method efficiently caches and responds to
30 | * requests for URLs in the manifest.
31 | * See https://goo.gl/S9QRab
32 | */
33 | self.__precacheManifest = [].concat(self.__precacheManifest || []);
34 | workbox.precaching.precacheAndRoute(self.__precacheManifest, {});
35 |
36 | workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("./index.html"), {
37 |
38 | blacklist: [/^\/_/,/\/[^/?]+\.[^/]+$/],
39 | });
40 |
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/static/js/2.2bc12ddb.chunk.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*!
8 | * cookie
9 | * Copyright(c) 2012-2014 Roman Shtylman
10 | * Copyright(c) 2015 Douglas Christopher Wilson
11 | * MIT Licensed
12 | */
13 |
14 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
15 |
16 | /**
17 | * @license
18 | * Copyright 2018-2021 Streamlit Inc.
19 | *
20 | * Licensed under the Apache License, Version 2.0 (the "License");
21 | * you may not use this file except in compliance with the License.
22 | * You may obtain a copy of the License at
23 | *
24 | * http://www.apache.org/licenses/LICENSE-2.0
25 | *
26 | * Unless required by applicable law or agreed to in writing, software
27 | * distributed under the License is distributed on an "AS IS" BASIS,
28 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29 | * See the License for the specific language governing permissions and
30 | * limitations under the License.
31 | */
32 |
33 | /** @license React v16.13.1
34 | * react-is.production.min.js
35 | *
36 | * Copyright (c) Facebook, Inc. and its affiliates.
37 | *
38 | * This source code is licensed under the MIT license found in the
39 | * LICENSE file in the root directory of this source tree.
40 | */
41 |
42 | /** @license React v16.14.0
43 | * react.production.min.js
44 | *
45 | * Copyright (c) Facebook, Inc. and its affiliates.
46 | *
47 | * This source code is licensed under the MIT license found in the
48 | * LICENSE file in the root directory of this source tree.
49 | */
50 |
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/static/js/main.a8b332f3.chunk.js:
--------------------------------------------------------------------------------
1 | (this.webpackJsonpstreamlit_extras_cookie_manager=this.webpackJsonpstreamlit_extras_cookie_manager||[]).push([[0],{5:function(e,t,a){e.exports=a(6)},6:function(e,t,a){"use strict";a.r(t);var s=a(4),n=a(0),i=null,o=new s.a;parent.CookieManager=o,window.CookieManager=o;n.a.events.addEventListener(n.a.RENDER_EVENT,(function(e){var t=e.detail,a=t.args,s=(t.disabled,t.theme,a.method),r=a.options,m=void 0===r?{}:r,p=m.name,l=m.value,g=m.expires,u=m.sameSite,c=m.secure,d=m.path,h={name:p,value:l,expires:new Date(g)||new Date((new Date).getTime()+432e5),sameSite:u||"strict",secure:c||!0,path:d||"/"},v={path:h.path,sameSite:h.sameSite},w="set"===s?o.set(p,l,h)||!0:"get"===s?o.get(p):"getAll"===s?o.getAll():"delete"===s?o.remove(p,v)||!0:null;w&&JSON.stringify(i)!=JSON.stringify(w)&&(i=w,n.a.setComponentValue(w),n.a.setComponentReady()),n.a.setFrameHeight(0)})),n.a.setComponentReady(),n.a.setFrameHeight(0)}},[[5,1,2]]]);
2 | //# sourceMappingURL=main.a8b332f3.chunk.js.map
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/static/js/main.a8b332f3.chunk.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["index.jsx"],"names":["lastOutput","cookies","Cookies","parent","window","Streamlit","events","addEventListener","RENDER_EVENT","event","detail","args","method","disabled","theme","options","name","value","expires","sameSite","secure","path","defaultedOptions","Date","getTime","defaultExpiryMinutes","removeOptions","output","set","get","getAll","remove","JSON","stringify","setComponentValue","setComponentReady","setFrameHeight"],"mappings":"oLAAA,yBAGIA,EAAa,KAOXC,EAAU,IAAIC,IAEpBC,OAAsB,cAAIF,EAC1BG,OAAsB,cAAIH,EAqC1BI,IAAUC,OAAOC,iBAAiBF,IAAUG,cAlCtB,SAACC,GACnB,MAAkCA,EAAMC,OAAhCC,EAAI,EAAJA,KACAC,GADc,EAARC,SAAe,EAALC,MACSH,EAAzBC,QAAO,EAAkBD,EAAjBI,eAAO,MAAG,GAAE,EACpBC,EAAiDD,EAAjDC,KAAMC,EAA2CF,EAA3CE,MAAOC,EAAoCH,EAApCG,QAASC,EAA2BJ,EAA3BI,SAAUC,EAAiBL,EAAjBK,OAAQC,EAASN,EAATM,KAE1CC,EAAmB,CACrB,KAAQN,EACR,MAASC,EACT,QAAW,IAAIM,KAAKL,IAAY,IAAIK,MAAK,IAAIA,MAAOC,UAAYC,OAChE,SAAYN,GAAY,SACxB,OAAUC,IAAU,EACpB,KAAQC,GAAQ,KAMdK,EAAgB,CAAEL,KAAMC,EAAiBD,KAAMF,SAAUG,EAAiBH,UAC1EQ,EACS,QAAXf,EAAmBX,EAAQ2B,IAAIZ,EAAMC,EAAOK,KAAqB,EAChD,QAAXV,EAAmBX,EAAQ4B,IAAIb,GACpB,WAAXJ,EAAsBX,EAAQ6B,SACnB,WAAXlB,EAAsBX,EAAQ8B,OAAOf,EAAMU,KAAkB,EAC7D,KAENC,GAAUK,KAAKC,UAAUjC,IAAegC,KAAKC,UAAUN,KACvD3B,EAAa2B,EACbtB,IAAU6B,kBAAkBP,GAC5BtB,IAAU8B,qBAGd9B,IAAU+B,eAAe,MAI7B/B,IAAU8B,oBACV9B,IAAU+B,eAAe,K","file":"static/js/main.a8b332f3.chunk.js","sourcesContent":["import Cookies from \"universal-cookie\"\nimport { Streamlit } from \"streamlit-component-lib\"\n\nlet lastOutput = null\n/*eslint-disable */\n// Called after successful set and remove\n// const changeListener = (params) => {\n// const { name, value, options } = params\n// console.log(name, value, options)\n// }\nconst cookies = new Cookies()\n// cookies.addChangeListener(changeListener)\nparent[\"CookieManager\"] = cookies\nwindow[\"CookieManager\"] = cookies\n/*eslint-enable */\n\nconst CookieManager = (event) => {\n const { args, disabled, theme } = event.detail\n const { method, options = {} } = args\n const { name, value, expires, sameSite, secure, path } = options\n const defaultExpiryMinutes = 720\n const defaultedOptions = {\n \"name\": name,\n \"value\": value,\n \"expires\": new Date(expires) || new Date(new Date().getTime() + defaultExpiryMinutes * 60000),\n \"sameSite\": sameSite || \"strict\",\n \"secure\": secure || true,\n \"path\": path || \"/\",\n //\"maxAge\"\n //\"domain\"\n //\"httpOnly\"\n }\n\n const removeOptions = { path: defaultedOptions.path, sameSite: defaultedOptions.sameSite }\n const output =\n method === \"set\" ? cookies.set(name, value, defaultedOptions) || true\n : method === \"get\" ? cookies.get(name)\n : method === \"getAll\" ? cookies.getAll()\n : method === \"delete\" ? cookies.remove(name, removeOptions) || true\n : null\n\n if (output && JSON.stringify(lastOutput) != JSON.stringify(output)) {\n lastOutput = output\n Streamlit.setComponentValue(output)\n Streamlit.setComponentReady()\n }\n\n Streamlit.setFrameHeight(0)\n}\n\nStreamlit.events.addEventListener(Streamlit.RENDER_EVENT, CookieManager)\nStreamlit.setComponentReady()\nStreamlit.setFrameHeight(0)\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/build/static/js/runtime-main.41b280b5.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,a,i=r[0],l=r[1],f=r[2],p=0,s=[];p0.2%",
22 | "not dead",
23 | "not op_mini all"
24 | ],
25 | "development": [
26 | "last 1 chrome version",
27 | "last 1 firefox version",
28 | "last 1 safari version"
29 | ]
30 | },
31 | "homepage": "."
32 | }
33 |
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Cookie Manager
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/streamlitextras/cookiemanager/frontend/src/index.jsx:
--------------------------------------------------------------------------------
1 | import Cookies from "universal-cookie"
2 | import { Streamlit } from "streamlit-component-lib"
3 |
4 | let lastOutput = null
5 | /*eslint-disable */
6 | // Called after successful set and remove
7 | // const changeListener = (params) => {
8 | // const { name, value, options } = params
9 | // console.log(name, value, options)
10 | // }
11 | const cookies = new Cookies()
12 | // cookies.addChangeListener(changeListener)
13 | parent["CookieManager"] = cookies
14 | window["CookieManager"] = cookies
15 | /*eslint-enable */
16 |
17 | const CookieManager = (event) => {
18 | const { args, disabled, theme } = event.detail
19 | const { method, options = {} } = args
20 | const { name, value, expires, sameSite, secure, path } = options
21 | const defaultExpiryMinutes = 720
22 | const defaultedOptions = {
23 | "name": name,
24 | "value": value,
25 | "expires": new Date(expires) || new Date(new Date().getTime() + defaultExpiryMinutes * 60000),
26 | "sameSite": sameSite || "strict",
27 | "secure": secure || true,
28 | "path": path || "/",
29 | //"maxAge"
30 | //"domain"
31 | //"httpOnly"
32 | }
33 |
34 | const removeOptions = { path: defaultedOptions.path, sameSite: defaultedOptions.sameSite }
35 | const output =
36 | method === "set" ? cookies.set(name, value, defaultedOptions) || true
37 | : method === "get" ? cookies.get(name)
38 | : method === "getAll" ? cookies.getAll()
39 | : method === "delete" ? cookies.remove(name, removeOptions) || true
40 | : null
41 |
42 | if (output && JSON.stringify(lastOutput) != JSON.stringify(output)) {
43 | lastOutput = output
44 | Streamlit.setComponentValue(output)
45 | Streamlit.setComponentReady()
46 | }
47 |
48 | Streamlit.setFrameHeight(0)
49 | }
50 |
51 | Streamlit.events.addEventListener(Streamlit.RENDER_EVENT, CookieManager)
52 | Streamlit.setComponentReady()
53 | Streamlit.setFrameHeight(0)
54 |
--------------------------------------------------------------------------------
/streamlitextras/logger/README.md:
--------------------------------------------------------------------------------
1 | # Streamlit Extras Logger
2 |
3 | This is mostly used internally by Streamlit Extras, however you can use it as well to log in your app.
4 | See the Loguru documentation on PyPI for more information.
5 |
6 | It will log to stdout (console/terminal) as well as to two files in `./logs`
7 |
8 |
9 | #### Basic usage
10 |
11 | ```Python
12 | import streamlit as st
13 | from streamlitextras.logger import log
14 |
15 | def main():
16 | log.debug("My app just started!")
17 | st.write("My app")
18 |
19 | if __name__ == "__main__":
20 | main()
21 | ```
22 |
23 | ### Advanced usage
24 |
25 | The files in logs will contain trailing dicts of st.session_state() which can be processed and used to display logs in a page like so:
26 |
27 | ```Python
28 |
29 | import os
30 | import pytz
31 | import streamlit as st
32 | from streamlitextras.webutils import get_user_timezone
33 | from streamlitextras.logger import log, process_log_line
34 |
35 | def log_page():
36 | log_file = "logs/debug_streamlit.log"
37 | if not os.path.exists(log_file):
38 | st.write("No log file.")
39 | return
40 |
41 | page_timezone = pytz.timezone(get_user_timezone())
42 |
43 | with open(log_file, "r") as f:
44 | log_contents = f.read()
45 |
46 | log_levels = {}
47 | log_lines = log_contents.split("\n")
48 |
49 | for line in log_lines:
50 | log_obj = process_log_line(line)
51 | if not log_obj:
52 | continue
53 | level = log_obj["level"]
54 | if level not in log_levels:
55 | log_levels[level] = []
56 | log_levels[level].append(log_obj)
57 |
58 | log_level_names = [str(name) for name in log_levels.keys()]
59 | option = st.selectbox("Log level:", log_level_names)
60 |
61 | timestamps = {}
62 | for log_obj in reversed(log_levels[option]):
63 | user = log_obj["extra"]["user"] if "user" in log_obj["extra"] else None
64 | time = log_obj["time"].astimezone(page_timezone)
65 | formatted_time = time.strftime("%a %d %b, %H:%M:%S")
66 | key = (formatted_time, user)
67 | if key not in timestamps:
68 | timestamps[key] = []
69 | timestamps[key].append(log_obj)
70 |
71 | for key, log_objects in timestamps.items():
72 | timestamp, timestamp_user = key
73 | columns = st.columns(2)
74 | info_display = f"""{timestamp} {timestamp_user}"""
75 | columns[0].markdown(info_display, unsafe_allow_html=True)
76 |
77 | count = 1
78 | counter = None
79 | last_line = None
80 | for log_obj in reversed(log_objects):
81 | extra = log_obj["extra"]
82 | user = extra["user"] if "user" in extra else None
83 |
84 | if count > 1:
85 | counter.write(count)
86 |
87 | if len(extra["session_state"]) == 0:
88 | if last_line and log_obj["message"] == last_line["message"]:
89 | count += 1
90 | else:
91 | columns[1].write(log_obj["message"])
92 | counter = columns[1].container()
93 | else:
94 | if last_line and log_obj["message"] == last_line["message"] and extra == last_line["extra"]:
95 | count += 1
96 | else:
97 | with columns[1].expander(log_obj["message"]):
98 | st.write(extra)
99 | counter = columns[1].container()
100 |
101 | if log_obj["exception"]:
102 | st.write(log_obj["exception"])
103 |
104 | last_line = log_obj
105 | st.write("---")
106 | ```
107 |
108 | See the `process_log_line()` definition for more information.
--------------------------------------------------------------------------------
/streamlitextras/logger/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import ast
4 | import loguru
5 | import dateutil.parser
6 | from loguru._logger import Logger
7 | import streamlit as st
8 | from streamlitextras.utils import repr_
9 |
10 | dev_emulation = os.environ.get("DEV_EMULATION", False)
11 |
12 | _LOGGER = loguru.logger
13 | log_folder = "logs"
14 | log_filename = "streamlit.log"
15 |
16 | module_filter = ""
17 | default_format = "{time} | {level: <8} | {name}:{module}:{function}:{file}:{line} | {message} | {extra}"
18 | detailed_format = "{time} | {level: <8} | {name}:{module}:{function}:{file.path}:{line} | {message} | {extra} | {exception} | {process.name}:{process} {thread.name}:{thread}"
19 |
20 | colour_format = (
21 | "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
22 | "{level: <8} | "
23 | "{name}:{module}:{function}:{file}:{line} | {extra} | "
24 | "\n{message}"
25 | )
26 |
27 | colour_detailed_format = (
28 | "{time:YYYY-MM-DD HH:mm:ss.SSS} | "
29 | "{level: <8} | "
30 | "{name}:{module}:{function}:{file.path}:{line} | "
31 | "\n{extra}{message} | "
32 | "{exception} | "
33 | "{process.name}:{process} @ {thread.name}:{thread}"
34 | )
35 | if dev_emulation:
36 | colour_format = colour_format.replace("file", "file.path")
37 |
38 | handlers = [
39 | {
40 | "sink": os.path.join(log_folder, log_filename),
41 | "rotation": "250 MB",
42 | "enqueue": True,
43 | "format": default_format,
44 | "filter": module_filter,
45 | "level": "INFO",
46 | "catch": True,
47 | },
48 | {
49 | "sink": os.path.join(log_folder, "debug_" + log_filename),
50 | "rotation": "250 MB",
51 | "enqueue": True,
52 | "format": detailed_format,
53 | "filter": module_filter,
54 | "level": "TRACE",
55 | "backtrace": True,
56 | "diagnose": True,
57 | "catch": True,
58 | },
59 | # Errors will be logged to sys.stdout
60 | # {"sink": sys.stderr,
61 | # "format": colour_detailed_format,
62 | # "filter": module_filter,
63 | # "level": "ERROR",
64 | # "backtrace": True,
65 | # "colorize": True},
66 | {
67 | "sink": sys.stdout,
68 | "format": colour_format,
69 | "filter": module_filter,
70 | "level": "DEBUG",
71 | "colorize": True,
72 | },
73 | ]
74 |
75 |
76 | def process_log_line(log_line: str):
77 | """
78 | Process a log line formatted from this module into a dictionary of its sections.
79 |
80 | :param str log_line: The log line to process
81 | """
82 | if not log_line:
83 | return None
84 | time, level, namespace, message, extra, exception, exec = log_line.split(" | ")
85 | level = level.strip()
86 | name, module, function_name, file_path, line = namespace.split(":")
87 |
88 | try:
89 | process, thread = exec.split(" @ ")
90 | except:
91 | process = exec.split(" ")[0]
92 | thread = " ".join(exec.split(" ")[1:])
93 | process_name, process_id = process.split(":")
94 | thread_name, thread_id = thread.split(":")
95 |
96 | log_obj = {
97 | "log_line": log_line,
98 | "level": level,
99 | "time": dateutil.parser.isoparse(time),
100 | "message": message,
101 | "extra": ast.literal_eval(extra),
102 | "exception": exception,
103 | "namespace": namespace,
104 | "name": name,
105 | "module": module,
106 | "function_name": function_name,
107 | "file_path": file_path,
108 | "line": line,
109 | "process_name": process_name,
110 | "process_id": process_id,
111 | "thread_name": thread_name,
112 | "thread_id": thread_id,
113 | }
114 | return log_obj
115 |
116 |
117 | def default_bind(include_session_state: bool = False):
118 | """
119 | Generate a dict from st.session_state to be stored with every log line
120 | """
121 | extra = {
122 | "user": repr(st.session_state.get("user", None)),
123 | }
124 |
125 | if include_session_state:
126 | extra["session_state"] = (
127 | {k: f"{v}" for k, v in st.session_state.to_dict().items()},
128 | )
129 |
130 | for k in list(extra.keys()):
131 | if not extra[k]:
132 | del extra[k]
133 |
134 | return extra
135 |
136 |
137 | def bind_log(extras=None) -> Logger:
138 | """
139 | Bind the logger to the session state dictionary
140 | """
141 | global log
142 | merged = {**default_bind(), **(extras if extras else {})}
143 |
144 | for k in list(merged):
145 | merged[k] = str(merged[k])
146 |
147 | log = _LOGGER.bind(**merged)
148 | return log
149 |
150 |
151 | log: Logger
152 |
153 |
154 | def __getattr__(name):
155 | global log
156 | if name == "log":
157 | log = bind_log()
158 | return log
159 | raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
160 |
161 |
162 | sinks = _LOGGER.configure(handlers=handlers, activation=[("", True)])
163 |
--------------------------------------------------------------------------------
/streamlitextras/logger/firebasesink.py:
--------------------------------------------------------------------------------
1 | from streamlitextras.authenticator.utils import handle_firebase_action
2 |
3 |
4 | def firestore_sink(message):
5 | """
6 | Loguru sink to store logs in firestore
7 | """
8 | raise NotImplementedError()
9 |
--------------------------------------------------------------------------------
/streamlitextras/router/README.md:
--------------------------------------------------------------------------------
1 | # Streamlit Extras Router
2 |
3 | Router is a class to manage pages and routing to them, either via browser query strings or via other paths in your code.
4 |
5 | #### Basic usage
6 |
7 | Note that `st.set_page_config` must be called before getting the router.
8 |
9 | ```Python
10 | import random
11 | import streamlit as st
12 | from streamlitextras.router import get_router
13 |
14 | router = None
15 | def main():
16 | global router
17 | pages = {
18 | "main": main_page,
19 | "other": another_page,
20 | }
21 | st.set_page_config(
22 | page_title="MyApp",
23 | layout="wide",
24 | initial_sidebar_state="auto"
25 | )
26 | router = get_router()
27 | router.delayed_init() # This is required to make sure current Router stays in session state
28 |
29 | computed_chance = random.randrange(10)
30 | if computed_chance > 1:
31 | router.route()
32 | else:
33 | router.route("other", computed_chance)
34 |
35 | def main_page(page_state = None):
36 | st.write("This is the main.")
37 |
38 | def another_page(page_state = None):
39 | st.write(f"This is another page, you're lucky to be here. Number {page_state} lucky.")
40 |
41 | if __name__ == "__main__":
42 | main()
43 | ```
44 |
45 | This will set the browser query string to `/?page_name=page_state` and render the page function associated with that page name in pages.
46 |
47 | `router.route()` takes the key from the `pages` dict as the first argument, and routes to that page, with no arguments will default to the first page in `pages`
48 |
49 | Due to a streamlit bug not being able to read empty query strings, page_state will show as `~` in the browser URL if there is none.
50 |
51 | Instead of using `router.route()` you can use `router.show_route_view()` which will bypass setting query strings and just run that page function:
52 |
53 | ```Python
54 | computed_chance = random.randrange(10)
55 | if computed_chance > 1:
56 | router.show_route_view()
57 | else:
58 | router.show_route_view("other", computed_chance)
59 | ```
60 |
61 | See the [API docs](https://streamlitextras.readthedocs.io/en/latest/api/streamlitextras.html) for more usage options, such as setting a callable pre-route function, or passing extra page_state/dependencies to every page_function.
62 |
63 | #### Advanced usage
64 |
65 | Below is an implementation that uses a routes folder module to manage pages.
66 |
67 | routes folder `__init__.py`
68 | ```Python
69 | import streamlitextras
70 |
71 | router: streamlitextras.router.Router
72 | auth: streamlitextras.authenticator.Authenticator
73 | cookie_manager: streamlitextras.cookiemanager.CookieManager
74 |
75 | def __getattr__(name):
76 | if name == "cookie_manager":
77 | cookie_manager = streamlitextras.cookiemanager.get_cookie_manager()
78 | return cookie_manager
79 | elif name == "auth":
80 | auth = streamlitextras.authenticator.get_auth("my_cookie")
81 | return auth
82 | elif name == "router":
83 | router = streamlitextras.router.get_router(pages, pre_route)
84 | return router
85 | raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
86 |
87 | from .preroute import pre_route
88 | from .userpage import user_page
89 | from .authpage import auth_page
90 | from .mainpage import main_page
91 |
92 | pages = {
93 | "main": main_page,
94 | "user": user_page,
95 | "auth": auth_page,
96 | }
97 |
98 | ```
99 |
100 | `main.py`
101 | ```Python
102 | import os
103 | import streamlit as st
104 | from streamlitextras.authenticator.exceptions import AuthException
105 | from streamlitextras.logger import log
106 | st.set_page_config(
107 | page_title="",
108 | layout="wide",
109 | initial_sidebar_state="auto"
110 | )
111 | import routes # NOTE: This has to be imported after st.set_page_config()
112 |
113 |
114 | router = None
115 | auth = None
116 | cookie_manager = None
117 | def main() -> None:
118 | router = routes.router
119 | auth = routes.auth
120 | cookie_manager = routes.cookie_manager
121 |
122 | # This is required to make sure current instances stay in session_state
123 | auth.delayed_init()
124 | router.delayed_init()
125 |
126 | try:
127 | auth_status = auth.auth_status
128 | user = auth.current_user
129 | log.info(f"Auth status: {auth_status} - User: {user.localId if user else user}")
130 | except AuthException as e:
131 | log.error(f"Error checking auth_status {e}")
132 | return router.show_route_view("auth")
133 |
134 | if auth_status and user:
135 | router.show_route_view(redirect_page_names=["auth"])
136 | else:
137 | router.show_route_view("auth")
138 |
139 | if __name__ == "__main__":
140 | main()
141 |
142 | ```
143 |
144 | For full reference see the [API docs](https://streamlitextras.readthedocs.io/en/latest/api/streamlitextras.html).
--------------------------------------------------------------------------------
/streamlitextras/storageservice.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import hashlib
3 | import binascii
4 | import datetime
5 | import streamlit as st
6 | from streamlit.runtime.uploaded_file_manager import UploadedFile
7 |
8 | from google.oauth2 import service_account
9 | from google.cloud import storage
10 | from google.cloud.storage.retry import _should_retry
11 | from google.api_core import retry
12 |
13 | from typing import Optional, Iterable, Union, Tuple
14 | from io import BytesIO
15 |
16 | # Default project and bucket
17 | default_gproject = st.secrets["gcp_service_account"]["project_id"]
18 | default_bucket = st.secrets["firebase"]["storageBucket"]
19 |
20 | # Create API client.
21 | credentials = service_account.Credentials.from_service_account_info(
22 | dict(st.secrets["gcp_service_account"])
23 | )
24 | client = storage.Client(credentials=credentials)
25 |
26 | _INITIAL_DELAY = 1.0 # seconds
27 | _MAXIMUM_DELAY = 4.0
28 | _DELAY_MULTIPLIER = 2.0
29 | _DEADLINE = 60.0
30 | retry_custom = retry.Retry(
31 | _should_retry, _INITIAL_DELAY, _MAXIMUM_DELAY, _DELAY_MULTIPLIER, _DEADLINE
32 | )
33 |
34 |
35 | def compute_bytes_md5hash(data: bytes):
36 | md5hex = hashlib.md5(data).hexdigest()
37 | encoded_bytes = base64.b64encode(binascii.unhexlify(md5hex))
38 | sig = encoded_bytes.rstrip(b"\n").decode("UTF-8")
39 | return sig
40 |
41 |
42 | def get_buckets(
43 | project: str = default_gproject,
44 | prefix: Optional[str] = None,
45 | page_size: int = 500,
46 | timeout: int = 8,
47 | retry: retry.Retry = retry_custom,
48 | print_names: bool = False,
49 | ) -> Iterable[storage.Bucket]:
50 | buckets = client.list_buckets(
51 | project=project,
52 | prefix=prefix,
53 | page_size=page_size,
54 | timeout=timeout,
55 | retry=retry,
56 | )
57 | if print_names:
58 | for bucket in buckets:
59 | print(bucket)
60 | return buckets
61 |
62 |
63 | def get_blobs(
64 | bucket_or_name: Union[str, storage.Bucket] = default_bucket,
65 | prefix: Optional[str] = None,
66 | page_size: int = 500,
67 | timeout: int = 8,
68 | retry: retry.Retry = retry_custom,
69 | print_names: bool = False,
70 | ) -> Optional[Iterable[storage.Blob]]:
71 | """
72 | Prints a list of all the blobs in the specified storage bucket,
73 | and returns them as an iterable
74 | """
75 | blobs = client.list_blobs(
76 | bucket_or_name, prefix=prefix, page_size=page_size, timeout=timeout, retry=retry
77 | )
78 | bucket_name = bucket_or_name if type(bucket_or_name) == str else bucket_or_name.name
79 | if print_names:
80 | for blob in blobs:
81 | print(blob)
82 |
83 | return blobs
84 |
85 |
86 | def get_bucket(
87 | bucket_name: str, timeout: int = 8, retry: retry.Retry = retry_custom
88 | ) -> Optional[storage.Bucket]:
89 | bucket = client.lookup_bucket(bucket_name, timeout=timeout, retry=retry)
90 | return bucket
91 |
92 |
93 | def create_bucket(
94 | bucket_name: str,
95 | project: str = default_gproject,
96 | billing_project: str = default_gproject,
97 | bucket_location: str = "us-east1",
98 | timeout: int = 8,
99 | retry: retry.Retry = retry_custom,
100 | ) -> storage.bucket.Bucket:
101 | bucket = client.create_bucket(
102 | bucket_name,
103 | location=bucket_location,
104 | project=project,
105 | user_project=billing_project,
106 | timeout=timeout,
107 | retry=retry,
108 | )
109 | return bucket
110 |
111 |
112 | def upload_blob_data(
113 | blob_name: str,
114 | blob_data: Union[UploadedFile, str],
115 | bucket_name: str = default_bucket,
116 | content_type: Optional[str] = None,
117 | timeout: int = 8,
118 | retry: retry.Retry = retry_custom,
119 | ) -> Tuple[storage.Blob, str]:
120 | bucket = client.bucket(bucket_name)
121 | blob = bucket.blob(blob_name)
122 |
123 | if blob.exists(client, timeout=timeout, retry=retry):
124 | return (blob, get_blob_url(blob))
125 |
126 | if type(blob_data) == UploadedFile:
127 | blob.upload_from_file(
128 | blob_data, content_type=content_type, timeout=timeout, retry=retry
129 | )
130 | elif type(blob_data) == str:
131 | blob.upload_from_string(
132 | blob_data, content_type=content_type, timeout=timeout, retry=retry
133 | )
134 |
135 | return (blob, get_blob_url(blob))
136 |
137 |
138 | def get_blob_url(blob: storage.Blob):
139 | url = blob.generate_signed_url(
140 | expiration=datetime.timedelta(minutes=60), version="v4", method="GET"
141 | )
142 | return url
143 |
144 |
145 | def download_blob_data(
146 | bucket_name: str, blob_name: str, return_type: str = "bytes"
147 | ) -> Union[BytesIO, str]:
148 | bucket = client.bucket(bucket_name)
149 | content = None
150 | if return_type == "bytes":
151 | content = bucket.blob(blob_name).download_as_bytes()
152 | elif return_type == "string":
153 | content = bucket.blob(blob_name).download_as_text(encoding="utf-8")
154 |
155 | return content
156 |
--------------------------------------------------------------------------------
/streamlitextras/threader/README.md:
--------------------------------------------------------------------------------
1 | # Streamlit Extras Threader
2 |
3 | Utility functions to make running threading.Threads easy in streamlit.
4 |
5 | #### Basic usage
6 |
7 | This requires `runOnSave = true` in `./streamlit/config.toml`,
8 | and you will need to create an empty file named `reruntrigger.py` in the root of your project.
9 |
10 | ```Python
11 | import time
12 | import streamlit as st
13 | import reruntrigger_default # This is required so the watcher can rerun from this file
14 | from streamlitextras.threader import lock, trigger_rerun, \
15 | streamlit_thread, get_thread, \
16 | last_trigger_time
17 |
18 | def main():
19 | thread_name = streamlit_thread(my_threaded_function, (5,))
20 | st.write("This should be here before my_threaded_function() is done!")
21 | st.button("Thread info", on_click=button_callback, args=(thread_name,))
22 |
23 | def button_callback(thread_name):
24 | # Sometimes streamlit will trigger button callbacks when re-running,
25 | # So we block them if we triggered a rerun recently
26 | if last_trigger_time() < 1:
27 | return
28 | my_thread = get_thread(thread_name)
29 | st.write(my_thread) # threading.Thread
30 |
31 | def my_threaded_function(time):
32 | time.sleep(time)
33 | with lock:
34 | # Do something that might interfere with other threads,
35 | # file operations or setting st.session_state
36 | pass
37 | print(f"Thread done! I slept for {time} seconds.")
38 |
39 | if __name__ == "__main__":
40 | main()
41 | ```
42 |
43 | #### Advanced usage
44 |
45 | Mostly you may want to use `streamlit_thread(my_threaded_function, rerun_st=False)` if you don't want streamlit to rerun after the thread.
46 |
47 | **NOTE** The rerun trigger will rerun all streamlit sessions for all users on your site.
48 |
49 | To get around this, generate or use a unique id you have for each user session,
50 | use it in this modules function arguments and then import the rerun trigger for that session e.g.
51 |
52 | ```Python
53 | import importlib
54 | unique_id = st.session_state.get("session_uid", generate_uniqueid())
55 | st.session_state["session_uid"] = unique_id
56 | importlib.import_module(f"reruntrigger_{unique_id}")
57 |
58 |
59 | from streamlitextras.threader import trigger_rerun
60 | st.button("Rerun for this session only", on_click=trigger_rerun, args=(unique_id,))
61 | ```
62 |
63 | See the [API docs](https://streamlitextras.readthedocs.io/en/latest/api/streamlitextras.html) or the source file for function argument reference.
64 |
--------------------------------------------------------------------------------
/streamlitextras/threader/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import time
3 | import inspect
4 | import threading
5 | from collections.abc import Callable, Iterable, Mapping
6 |
7 | from streamlit.runtime.scriptrunner import (
8 | add_script_run_ctx,
9 | get_script_run_ctx,
10 | RerunException,
11 | ScriptRunContext,
12 | )
13 | from typing import Any, Callable, Optional
14 |
15 | # script_dir = os.path.dirname(os.path.realpath(__file__))
16 | script_dir = os.getcwd()
17 |
18 | default_id = "default"
19 |
20 |
21 | def trigger_file_path(unique_id: str = default_id) -> str:
22 | trigger_file_path = os.path.join(script_dir, f"reruntrigger_{unique_id}.py")
23 | if not os.path.exists(trigger_file_path):
24 | with open(trigger_file_path, "w") as f:
25 | f.write(f"timestamp = {time.time()}")
26 | return trigger_file_path
27 |
28 |
29 | lock = threading.Lock()
30 |
31 |
32 | def last_trigger_time(unique_id: str = default_id) -> int:
33 | """
34 | Returns the seconds since last writing the trigger file
35 | """
36 | this_trigger_file_path = trigger_file_path(unique_id)
37 | if not os.path.exists(this_trigger_file_path):
38 | return 9999
39 | modified_time = os.path.getmtime(this_trigger_file_path)
40 | modified_time_seconds = time.time() - modified_time
41 | return modified_time_seconds
42 |
43 |
44 | def trigger_rerun(
45 | unique_id: str = default_id, last_write_margin: int = 1, delay: int = 0
46 | ) -> None:
47 | """
48 | Triggers treamlit to rerun the current page state.
49 | runOnSave must be set to true in config.toml
50 |
51 | :param str unique_id: Unique ID to be triggered, should be set per session e.g. user id or a hash you create in their session state.
52 | :param int last_write_margin:
53 | If the file was modified less than this many seconds ago, the rerun will not be performed
54 | :param int delay: sleep for this many seconds before writing the rerun trigger
55 | """
56 | if delay:
57 | time.sleep(delay)
58 |
59 | with lock:
60 | modified_time_seconds = last_trigger_time(unique_id)
61 | if last_write_margin == 0 or modified_time_seconds > last_write_margin:
62 | frame = inspect.currentframe()
63 | caller = frame.f_back.f_code.co_name
64 | caller_caller = frame.f_back.f_back.f_code.co_name
65 | trigger_file = trigger_file_path(unique_id)
66 | print(
67 | "Writing trigger",
68 | trigger_file,
69 | f"from `{caller_caller}.{caller}`",
70 | flush=True,
71 | )
72 | with open(trigger_file, "w") as f:
73 | f.write(f"timestamp = {time.time()}")
74 | # https://github.com/streamlit/streamlit/issues/1792
75 | # https://discuss.streamlit.io/t/using-streamlit-with-multithreading/30990
76 | # https://discuss.streamlit.io/t/how-to-run-a-subprocess-programs-using-thread-inside-streamlit/2440/2
77 | # https://discuss.streamlit.io/t/how-to-monitor-the-filesystem-and-have-streamlit-updated-when-some-files-are-modified/822/3
78 |
79 |
80 | def thread_wrapper(
81 | thread_func,
82 | rerun_st=True,
83 | last_write_margin: int = 1,
84 | delay: int = 0,
85 | trigger_unique_id: str = default_id,
86 | *args,
87 | **kwargs,
88 | ) -> None:
89 | """
90 | Wrapper for running thread functions
91 | For parameters see streamlit_thread() and trigger_rerun()
92 | """
93 | # print("Hashseed in thread:", os.environ.get("PYTHONHASHSEED", False))
94 | thread_func(*args, **kwargs)
95 | if rerun_st is True:
96 | trigger_rerun(trigger_unique_id, last_write_margin, delay)
97 |
98 |
99 | def streamlit_thread(
100 | thread_func: Callable,
101 | args: tuple = (),
102 | kwargs: dict = {},
103 | rerun_st: bool = True,
104 | last_write_margin: int = 1,
105 | delay: int = 0,
106 | script_run_context: ScriptRunContext | None = None,
107 | autostart: bool = True,
108 | trigger_unique_id: str = default_id,
109 | error_handler: Callable | None = None,
110 | ) -> str:
111 | """
112 | Spawns and starts a threading.Thread that runs thread_func with the passed args and kwargs
113 |
114 | :param Callable thread_func: The function to run in the thread
115 | :param tuple args: The args to pass to the function in the thread
116 | :param dict kwargs: The kwargs to pass to the function in the thread
117 | :param bool rerun_st: Whether to rerun streamlit after the thread function finishes
118 |
119 | :param Callable error_handler: Error handler function that takes the thread exception as an argument
120 |
121 | :returns: The name of the thread. Can use get_thread to get the threading.Thread instance
122 | """
123 | # print("Thread entry hashseed:", os.environ.get("PYTHONHASHSEED", False))
124 | args = (thread_func, rerun_st, last_write_margin, delay, trigger_unique_id, *args)
125 | thread = PropagatingThread(
126 | target=thread_wrapper, error_handler=error_handler, args=args, kwargs=kwargs
127 | )
128 |
129 | if not script_run_context:
130 | script_run_context = get_script_run_ctx()
131 | add_script_run_ctx(thread, script_run_context)
132 |
133 | time.sleep(0.4)
134 | if autostart is True:
135 | thread.start()
136 |
137 | return thread.name
138 |
139 |
140 | def get_thread(thread_name) -> Optional[threading.Thread]:
141 | """
142 | Gets the threading.Thread instance thats name attribute matches thread_name
143 |
144 | :param thread_name: The name attribute of the thread to look for.
145 |
146 | :returns: The threading.Thread or None if theres no thread with the supplied thread_name
147 | """
148 | threads = threading.enumerate()
149 | target_thread = None
150 | for thread in threads:
151 | if thread.name == thread_name:
152 | target_thread = thread
153 | break
154 | return target_thread
155 |
156 |
157 | class PropagatingThread(threading.Thread):
158 | def __init__(self, *args, **kwargs) -> None:
159 | self.error_handler = kwargs.get("error_handler", None)
160 | del kwargs["error_handler"]
161 | super().__init__(*args, **kwargs)
162 |
163 | def run(self):
164 | self.exc = None
165 | try:
166 | self.ret = self._target(*self._args, **self._kwargs)
167 | except RerunException as e:
168 | self.exc = e
169 | except BaseException as e:
170 | self.exc = e
171 | if self.error_handler and callable(self.error_handler):
172 | self.error_handler(e)
173 | else:
174 | raise
175 |
176 | def join(self, timeout=None):
177 | super(PropagatingThread, self).join(timeout)
178 | if self.exc:
179 | if self.error_handler and callable(self.error_handler):
180 | self.error_handler(self.exc)
181 | else:
182 | raise self.exc
183 | # raise RuntimeError('Exception in thread') from self.exc
184 | return self.ret
185 |
--------------------------------------------------------------------------------
/streamlitextras/utils.py:
--------------------------------------------------------------------------------
1 | # File contains utility or helper functions
2 | import os
3 | from typing import Optional
4 | from streamlit.runtime.uploaded_file_manager import UploadedFile
5 |
6 |
7 | def repr_(
8 | cls, ignore_keys: Optional[list[str]] = None, only_keys: Optional[list[str]] = None
9 | ) -> str:
10 | """
11 | Returns a string detailing a class attributes from cls.__dict__
12 | Makes nice printing for __repr__ implementations
13 |
14 | :param ignore_keys: If provided, these keys will be not be included in the attribute string
15 | :param only_keys: If provided, these keys will be the only ones included in the attribute string
16 | """
17 | if not hasattr(cls, "__dict__"):
18 | return repr(cls)
19 | if not ignore_keys or only_keys:
20 | ignore_keys = []
21 | classname = cls.__class__.__name__
22 |
23 | try:
24 | args = ", ".join(
25 | [
26 | f"{k}={repr(v)}"
27 | for (k, v) in cls.__dict__.items()
28 | if k not in ignore_keys and (k in only_keys if only_keys else True)
29 | ]
30 | )
31 | except RecursionError:
32 | args = "Too much recursion"
33 | return f"{classname}({hex(id(cls))}, {args})"
34 |
35 |
36 | def save_file(st_file_object: UploadedFile, to_path: str) -> str:
37 | """
38 | Saves a streamlit UploadedFile BytesIO object to the given relative path
39 |
40 | :param UploadedFile st_file_object: The UploadedFile bytes object to save to disk - contains filename and metadata
41 | :param str to_path: The relative path to a folder to save the file to
42 | :returns str: Will return the relative path which can be used as a URL
43 | """
44 | os.makedirs(to_path, exist_ok=True)
45 | path = os.path.join(to_path, st_file_object.name)
46 | with open(path, "wb") as f:
47 | f.write(st_file_object.getbuffer())
48 | return "/" + path
49 |
50 |
51 | def where_stack():
52 | import inspect
53 |
54 | stack = inspect.stack()
55 | the_class = stack[1][0].f_locals["self"].__class__.__name__
56 | the_method = stack[1][0].f_code.co_name
57 |
58 | print("I was called by {}.{}()".format(the_class, the_method))
59 |
--------------------------------------------------------------------------------
/streamlitextras/webutils.py:
--------------------------------------------------------------------------------
1 | import time
2 | import html
3 | import uuid
4 | import base64
5 | import streamlit as st
6 | from io import BytesIO
7 | from typing import Union, Optional
8 |
9 | from streamlit_javascript import st_javascript
10 |
11 | from streamlit.runtime.uploaded_file_manager import UploadedFile
12 |
13 |
14 | def stxs_javascript(source: str) -> None:
15 | """
16 | Runs javascript on the top level context of the page.
17 |
18 | Does this by embedding an iframe that attaches a script tag to its parent.
19 |
20 | :param str source: The script source to embed in the element
21 | """
22 | div_id = uuid.uuid4()
23 |
24 | wrapped_source = f"(async () => {{{source}}})()"
25 |
26 | st.markdown(
27 | f"""
28 |
29 |
38 |
39 | """,
40 | unsafe_allow_html=True,
41 | )
42 | time.sleep(0.1)
43 |
44 | return True
45 |
46 |
47 | def get_user_timezone(default_tz: Optional[str] = None) -> Optional[str]:
48 | """
49 | Uses javascript to get the tz database name for the browser/users timezone
50 |
51 | :param Optional[str] default_tz: value to return if the operation fails. Defaults to UTC
52 |
53 | :returns Optional[str]:
54 | The tz database name for the timezone,
55 | or None if the operation fails and default_tz isn't set, or isn't supported by the browser (unlikely)
56 | """
57 | if not default_tz:
58 | default_tz = "Etc/UTC"
59 |
60 | timezone = st_javascript(
61 | """await (async () => {
62 | const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
63 | console.log(userTimezone)
64 | return userTimezone
65 | })().then(returnValue => returnValue)"""
66 | )
67 | if timezone == 0: # st_javascript returns 0 for null/undefined
68 | timezone = default_tz
69 | return timezone
70 |
71 |
72 | def scroll_page(x: int = 0, y: int = 0):
73 | """
74 | Runs javascript to scroll the streamlit main section
75 | Defaults to scrolling to the top if no arguments are passed
76 |
77 | :param int x: the x coordinate to scroll to
78 | :param int y: the y coordinate to scroll to
79 | """
80 | stxs_javascript(
81 | f"""parent.document.querySelector(`section[class*="main"`).scroll({x}, {y})"""
82 | )
83 |
84 |
85 | def bytes_to_data_uri(
86 | byteslike_object: Union[BytesIO, UploadedFile, bytes],
87 | mime_type: Optional[str] = None,
88 | ) -> str:
89 | """
90 | Creates a data URI from a bytesIO object
91 |
92 | :param Union[BytesIO, UploadedFile] byteslike_object: BytesIO or bytes or any class that inherits them
93 | :param str mime_type: The mimetype to set on the data URI
94 | :returns: The data URI as a string
95 | """
96 | data = None
97 | try:
98 | data = byteslike_object.getvalue()
99 | except:
100 | data = byteslike_object
101 | if not mime_type:
102 | mime_type = "application/octet-stream"
103 | uri = f"data:{mime_type};base64,{base64.b64encode(data).decode()}"
104 | return uri
105 |
106 |
107 | def trigger_download(download_uri: str, filename: str):
108 | """
109 | Uses javascript and a data URI on a link element to trigger a download for the user
110 |
111 | :param str download_uri: properly formatted data uri to place on link elements href
112 | :param str filename: filename to be placed on the link elements download attribute
113 | """
114 | auto_downloaded = stxs_javascript(
115 | f"""(async () => {{
116 | console.log("Creating download link..")
117 | var link = document.createElement('a');
118 | link.innerText = ""
119 | link.href = "{download_uri}";
120 | link.target = "_blank";
121 | link.download = "{filename}";
122 | link.click();
123 | await new Promise(r => setTimeout(r, 2000));
124 | //window.open(link.href, "_blank")
125 | }})();"""
126 | )
127 | # Give the page some time to render the element before the page rerenders
128 | time.sleep(1)
129 | return auto_downloaded
130 |
131 |
132 | def convert_millis(milliseconds: int, always_include_hours: bool = False) -> str:
133 | """
134 | Convert milliseconds to a string timestamp in the format HH:MM:SS or MM:SS
135 |
136 | :param int milliseconds: The amount of milliseconds to convert to a timestamp
137 | :param bool always_include_hours: Always include the HH: part of the timestamp even if 00.
138 | """
139 | seconds = int((milliseconds / 1000) % 60)
140 | minutes = int((milliseconds / (1000 * 60)) % 60)
141 | hours = int((milliseconds / (1000 * 60 * 60)) % 24)
142 | timestamp_text = ""
143 | if hours > 0:
144 | timestamp_text += f"{hours:02d}:{minutes:02d}:{seconds:02d}"
145 | else:
146 | if always_include_hours:
147 | timestamp_text += f"{hours:02d}:"
148 | timestamp_text += f"{minutes:02d}:{seconds:02d}"
149 |
150 | return timestamp_text
151 |
--------------------------------------------------------------------------------