Mise à jour de Monitor.py et autres scripts
This commit is contained in:
307
myenv/lib/python3.11/site-packages/streamlit/__init__.py
Normal file
307
myenv/lib/python3.11/site-packages/streamlit/__init__.py
Normal file
@@ -0,0 +1,307 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# isort: skip_file
|
||||
|
||||
"""Streamlit.
|
||||
|
||||
How to use Streamlit in 3 seconds:
|
||||
|
||||
1. Write an app
|
||||
>>> import streamlit as st
|
||||
>>> st.write(anything_you_want)
|
||||
|
||||
2. Run your app
|
||||
$ streamlit run my_script.py
|
||||
|
||||
3. Use your app
|
||||
A new tab will open on your browser. That's your Streamlit app!
|
||||
|
||||
4. Modify your code, save it, and watch changes live on your browser.
|
||||
|
||||
Take a look at the other commands in this module to find out what else
|
||||
Streamlit can do:
|
||||
|
||||
>>> dir(streamlit)
|
||||
|
||||
Or try running our "Hello World":
|
||||
|
||||
$ streamlit hello
|
||||
|
||||
For more detailed info, see https://docs.streamlit.io.
|
||||
"""
|
||||
|
||||
# IMPORTANT: Prefix with an underscore anything that the user shouldn't see.
|
||||
|
||||
import os as _os
|
||||
|
||||
# Set Matplotlib backend to avoid a crash.
|
||||
# The default Matplotlib backend crashes Python on OSX when run on a thread
|
||||
# that's not the main thread, so here we set a safer backend as a fix.
|
||||
# This fix is OS-independent. We didn't see a good reason to make this
|
||||
# Mac-only. Consistency within Streamlit seemed more important.
|
||||
# IMPORTANT: This needs to run on top of all imports before any other
|
||||
# import of matplotlib could happen.
|
||||
_os.environ["MPLBACKEND"] = "Agg"
|
||||
|
||||
|
||||
# Must be at the top, to avoid circular dependency.
|
||||
from streamlit import logger as _logger
|
||||
from streamlit import config as _config
|
||||
from streamlit.deprecation_util import deprecate_func_name as _deprecate_func_name
|
||||
from streamlit.version import STREAMLIT_VERSION_STRING as _STREAMLIT_VERSION_STRING
|
||||
|
||||
# Give the package a version.
|
||||
__version__ = _STREAMLIT_VERSION_STRING
|
||||
|
||||
# DeltaGenerator methods:
|
||||
# We initialize them here so that it is clear where they are instantiated.
|
||||
# Further, it helps us to break circular imports because the DeltaGenerator
|
||||
# imports the different elements but some elements also require DeltaGenerator
|
||||
# functions such as the dg_stack. Now, elements that require DeltaGenerator functions
|
||||
# can import the singleton module.
|
||||
from streamlit.delta_generator_singletons import (
|
||||
DeltaGeneratorSingleton as _DeltaGeneratorSingleton,
|
||||
)
|
||||
from streamlit.delta_generator import DeltaGenerator as _DeltaGenerator
|
||||
from streamlit.elements.lib.mutable_status_container import (
|
||||
StatusContainer as _StatusContainer,
|
||||
)
|
||||
from streamlit.elements.lib.dialog import Dialog as _Dialog
|
||||
|
||||
# instantiate the DeltaGeneratorSingleton
|
||||
_dg_singleton = _DeltaGeneratorSingleton(
|
||||
delta_generator_cls=_DeltaGenerator,
|
||||
status_container_cls=_StatusContainer,
|
||||
dialog_container_cls=_Dialog,
|
||||
)
|
||||
_main = _dg_singleton._main_dg
|
||||
sidebar = _dg_singleton._sidebar_dg
|
||||
_event = _dg_singleton._event_dg
|
||||
_bottom = _dg_singleton._bottom_dg
|
||||
|
||||
|
||||
from streamlit.elements.dialog_decorator import (
|
||||
dialog_decorator as _dialog_decorator,
|
||||
experimental_dialog_decorator as _experimental_dialog_decorator,
|
||||
)
|
||||
from streamlit.runtime.caching import (
|
||||
cache_resource as _cache_resource,
|
||||
cache_data as _cache_data,
|
||||
cache as _cache,
|
||||
)
|
||||
from streamlit.runtime.connection_factory import (
|
||||
connection_factory as _connection,
|
||||
)
|
||||
from streamlit.runtime.fragment import (
|
||||
experimental_fragment as _experimental_fragment,
|
||||
fragment as _fragment,
|
||||
)
|
||||
from streamlit.runtime.metrics_util import gather_metrics as _gather_metrics
|
||||
from streamlit.runtime.secrets import secrets_singleton as _secrets_singleton
|
||||
from streamlit.runtime.context import ContextProxy as _ContextProxy
|
||||
from streamlit.runtime.state import (
|
||||
SessionStateProxy as _SessionStateProxy,
|
||||
QueryParamsProxy as _QueryParamsProxy,
|
||||
)
|
||||
from streamlit.user_info import (
|
||||
UserInfoProxy as _UserInfoProxy,
|
||||
login as _login,
|
||||
logout as _logout,
|
||||
)
|
||||
from streamlit.commands.experimental_query_params import (
|
||||
get_query_params as _get_query_params,
|
||||
set_query_params as _set_query_params,
|
||||
)
|
||||
|
||||
import streamlit.column_config as _column_config
|
||||
|
||||
# Modules that the user should have access to. These are imported with the "as" syntax
|
||||
# and the same name; note that renaming the import with "as" does not make it an
|
||||
# explicit export. In this case, you should import it with an underscore to make clear
|
||||
# that it is internal and then assign it to a variable with the new intended name.
|
||||
# You can check the export behavior by running 'mypy --strict example_app.py', which
|
||||
# disables implicit_reexport, where you use the respective command in the example_app.py
|
||||
# Streamlit app.
|
||||
|
||||
from streamlit.commands.echo import echo as echo
|
||||
from streamlit.commands.logo import logo as logo
|
||||
from streamlit.commands.navigation import navigation as navigation
|
||||
from streamlit.navigation.page import Page as Page
|
||||
from streamlit.elements.spinner import spinner as spinner
|
||||
|
||||
from streamlit.commands.page_config import set_page_config as set_page_config
|
||||
from streamlit.commands.execution_control import (
|
||||
stop as stop,
|
||||
rerun as rerun,
|
||||
switch_page as switch_page,
|
||||
)
|
||||
|
||||
|
||||
def _update_logger() -> None:
|
||||
_logger.set_log_level(_config.get_option("logger.level").upper())
|
||||
_logger.update_formatter()
|
||||
_logger.init_tornado_logs()
|
||||
|
||||
|
||||
# Make this file only depend on config option in an asynchronous manner. This
|
||||
# avoids a race condition when another file (such as a test file) tries to pass
|
||||
# in an alternative config.
|
||||
_config.on_config_parsed(_update_logger, True)
|
||||
|
||||
secrets = _secrets_singleton
|
||||
|
||||
altair_chart = _main.altair_chart
|
||||
area_chart = _main.area_chart
|
||||
audio = _main.audio
|
||||
audio_input = _main.audio_input
|
||||
badge = _main.badge
|
||||
balloons = _main.balloons
|
||||
bar_chart = _main.bar_chart
|
||||
bokeh_chart = _main.bokeh_chart
|
||||
button = _main.button
|
||||
caption = _main.caption
|
||||
camera_input = _main.camera_input
|
||||
chat_message = _main.chat_message
|
||||
chat_input = _main.chat_input
|
||||
checkbox = _main.checkbox
|
||||
code = _main.code
|
||||
columns = _main.columns
|
||||
tabs = _main.tabs
|
||||
container = _main.container
|
||||
dataframe = _main.dataframe
|
||||
data_editor = _main.data_editor
|
||||
date_input = _main.date_input
|
||||
divider = _main.divider
|
||||
download_button = _main.download_button
|
||||
expander = _main.expander
|
||||
feedback = _main.feedback
|
||||
pydeck_chart = _main.pydeck_chart
|
||||
empty = _main.empty
|
||||
error = _main.error
|
||||
exception = _main.exception
|
||||
file_uploader = _main.file_uploader
|
||||
form = _main.form
|
||||
form_submit_button = _main.form_submit_button
|
||||
graphviz_chart = _main.graphviz_chart
|
||||
header = _main.header
|
||||
help = _main.help
|
||||
html = _main.html
|
||||
image = _main.image
|
||||
info = _main.info
|
||||
json = _main.json
|
||||
latex = _main.latex
|
||||
line_chart = _main.line_chart
|
||||
link_button = _main.link_button
|
||||
map = _main.map
|
||||
markdown = _main.markdown
|
||||
metric = _main.metric
|
||||
multiselect = _main.multiselect
|
||||
number_input = _main.number_input
|
||||
page_link = _main.page_link
|
||||
pills = _main.pills
|
||||
plotly_chart = _main.plotly_chart
|
||||
popover = _main.popover
|
||||
progress = _main.progress
|
||||
pyplot = _main.pyplot
|
||||
radio = _main.radio
|
||||
scatter_chart = _main.scatter_chart
|
||||
selectbox = _main.selectbox
|
||||
select_slider = _main.select_slider
|
||||
segmented_control = _main.segmented_control
|
||||
slider = _main.slider
|
||||
snow = _main.snow
|
||||
subheader = _main.subheader
|
||||
success = _main.success
|
||||
table = _main.table
|
||||
text = _main.text
|
||||
text_area = _main.text_area
|
||||
text_input = _main.text_input
|
||||
toggle = _main.toggle
|
||||
time_input = _main.time_input
|
||||
title = _main.title
|
||||
vega_lite_chart = _main.vega_lite_chart
|
||||
video = _main.video
|
||||
warning = _main.warning
|
||||
write = _main.write
|
||||
write_stream = _main.write_stream
|
||||
color_picker = _main.color_picker
|
||||
status = _main.status
|
||||
|
||||
# Events - Note: these methods cannot be called directly on sidebar
|
||||
# (ex: st.sidebar.toast)
|
||||
toast = _event.toast
|
||||
|
||||
# Config
|
||||
# We add the metrics tracking here, since importing
|
||||
# gather_metrics in config causes a circular dependency
|
||||
get_option = _gather_metrics("get_option", _config.get_option)
|
||||
set_option = _gather_metrics("set_option", _config.set_user_option)
|
||||
|
||||
# Session State
|
||||
session_state = _SessionStateProxy()
|
||||
|
||||
query_params = _QueryParamsProxy()
|
||||
|
||||
context = _ContextProxy()
|
||||
|
||||
# Caching
|
||||
cache_data = _cache_data
|
||||
cache_resource = _cache_resource
|
||||
# `st.cache` is deprecated and should be removed soon
|
||||
cache = _cache
|
||||
|
||||
# Namespaces
|
||||
column_config = _column_config
|
||||
|
||||
# Connection
|
||||
connection = _connection
|
||||
|
||||
# Fragment and dialog
|
||||
dialog = _dialog_decorator
|
||||
fragment = _fragment
|
||||
|
||||
|
||||
# Auth
|
||||
login = _login
|
||||
logout = _logout
|
||||
|
||||
# Experimental APIs
|
||||
experimental_audio_input = _main.experimental_audio_input
|
||||
experimental_dialog = _experimental_dialog_decorator
|
||||
experimental_fragment = _experimental_fragment
|
||||
experimental_user = _UserInfoProxy()
|
||||
|
||||
_EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG = "Refer to our [docs page](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.query_params) for more information."
|
||||
|
||||
experimental_get_query_params = _deprecate_func_name(
|
||||
_get_query_params,
|
||||
"experimental_get_query_params",
|
||||
"2024-04-11",
|
||||
_EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG,
|
||||
name_override="query_params",
|
||||
)
|
||||
experimental_set_query_params = _deprecate_func_name(
|
||||
_set_query_params,
|
||||
"experimental_set_query_params",
|
||||
"2024-04-11",
|
||||
_EXPERIMENTAL_QUERY_PARAMS_DEPRECATE_MSG,
|
||||
name_override="query_params",
|
||||
)
|
||||
|
||||
|
||||
# make it possible to call streamlit.components.v1.html etc. by importing it here
|
||||
# import in the very end to avoid partially-initialized module import errors, because
|
||||
# streamlit.components.v1 also uses some streamlit imports
|
||||
import streamlit.components.v1 # noqa: F401
|
||||
20
myenv/lib/python3.11/site-packages/streamlit/__main__.py
Normal file
20
myenv/lib/python3.11/site-packages/streamlit/__main__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from streamlit.web.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Set prog_name so that the Streamlit server sees the same command line
|
||||
# string whether streamlit is called directly or via `python -m streamlit`.
|
||||
main(prog_name="streamlit")
|
||||
219
myenv/lib/python3.11/site-packages/streamlit/auth_util.py
Normal file
219
myenv/lib/python3.11/site-packages/streamlit/auth_util.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.errors import StreamlitAuthError
|
||||
from streamlit.runtime.secrets import AttrDict, secrets_singleton
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class ProviderTokenPayload(TypedDict):
|
||||
provider: str
|
||||
exp: int
|
||||
|
||||
|
||||
class AuthCache:
|
||||
"""Simple cache implementation for storing info required for Authlib."""
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {}
|
||||
|
||||
def get(self, key):
|
||||
return self.cache.get(key)
|
||||
|
||||
# for set method, we are follow the same signature used in Authlib
|
||||
# the expires_in is not used in our case
|
||||
def set(self, key, value, expires_in):
|
||||
self.cache[key] = value
|
||||
|
||||
def get_dict(self):
|
||||
return self.cache
|
||||
|
||||
def delete(self, key):
|
||||
self.cache.pop(key, None)
|
||||
|
||||
|
||||
def is_authlib_installed() -> bool:
|
||||
"""Check if Authlib is installed."""
|
||||
try:
|
||||
import authlib # type: ignore[import-untyped]
|
||||
|
||||
authlib_version = authlib.__version__
|
||||
authlib_version_tuple = tuple(map(int, authlib_version.split(".")))
|
||||
|
||||
if authlib_version_tuple < (1, 3, 2):
|
||||
return False
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_signing_secret() -> str:
|
||||
"""Get the cookie signing secret from the configuration or secrets.toml."""
|
||||
signing_secret: str = config.get_option("server.cookieSecret")
|
||||
if secrets_singleton.load_if_toml_exists():
|
||||
auth_section = secrets_singleton.get("auth")
|
||||
if auth_section:
|
||||
signing_secret = auth_section.get("cookie_secret", signing_secret)
|
||||
return signing_secret
|
||||
|
||||
|
||||
def get_secrets_auth_section() -> AttrDict:
|
||||
auth_section = AttrDict({})
|
||||
"""Get the 'auth' section of the secrets.toml."""
|
||||
if secrets_singleton.load_if_toml_exists():
|
||||
auth_section = cast("AttrDict", secrets_singleton.get("auth"))
|
||||
|
||||
return auth_section
|
||||
|
||||
|
||||
def encode_provider_token(provider: str) -> str:
|
||||
"""Returns a signed JWT token with the provider and expiration time."""
|
||||
try:
|
||||
from authlib.jose import jwt # type: ignore[import-untyped]
|
||||
except ImportError:
|
||||
raise StreamlitAuthError(
|
||||
"""To use authentication features, you need to install Authlib>=1.3.2, e.g. via `pip install Authlib`."""
|
||||
) from None
|
||||
|
||||
header = {"alg": "HS256"}
|
||||
payload = {
|
||||
"provider": provider,
|
||||
"exp": datetime.now(timezone.utc) + timedelta(minutes=2),
|
||||
}
|
||||
provider_token: bytes = jwt.encode(header, payload, get_signing_secret())
|
||||
# JWT token is a byte string, so we need to decode it to a URL compatible string
|
||||
return provider_token.decode("latin-1")
|
||||
|
||||
|
||||
def decode_provider_token(provider_token: str) -> ProviderTokenPayload:
|
||||
"""Decode the JWT token and validate the claims."""
|
||||
try:
|
||||
from authlib.jose import JoseError, JWTClaims, jwt
|
||||
except ImportError:
|
||||
raise StreamlitAuthError(
|
||||
"""To use authentication features, you need to install Authlib>=1.3.2, e.g. via `pip install Authlib`."""
|
||||
) from None
|
||||
|
||||
# Our JWT token is short-lived (2 minutes), so we check here that it contains
|
||||
# the 'exp' (and it is not expired), and 'provider' field exists.
|
||||
claim_options = {"exp": {"essential": True}, "provider": {"essential": True}}
|
||||
try:
|
||||
payload: JWTClaims = jwt.decode(
|
||||
provider_token, get_signing_secret(), claims_options=claim_options
|
||||
)
|
||||
payload.validate()
|
||||
except JoseError as e:
|
||||
raise StreamlitAuthError(f"Error decoding provider token: {e}") from None
|
||||
|
||||
return cast("ProviderTokenPayload", payload)
|
||||
|
||||
|
||||
def generate_default_provider_section(auth_section) -> dict[str, Any]:
|
||||
"""Generate a default provider section for the 'auth' section of secrets.toml."""
|
||||
default_provider_section = {}
|
||||
if auth_section.get("client_id"):
|
||||
default_provider_section["client_id"] = auth_section.get("client_id")
|
||||
if auth_section.get("client_secret"):
|
||||
default_provider_section["client_secret"] = auth_section.get("client_secret")
|
||||
if auth_section.get("server_metadata_url"):
|
||||
default_provider_section["server_metadata_url"] = auth_section.get(
|
||||
"server_metadata_url"
|
||||
)
|
||||
if auth_section.get("client_kwargs"):
|
||||
default_provider_section["client_kwargs"] = auth_section.get(
|
||||
"client_kwargs"
|
||||
).to_dict()
|
||||
return default_provider_section
|
||||
|
||||
|
||||
def validate_auth_credentials(provider: str) -> None:
|
||||
"""Validate the general auth credentials and auth credentials for the given
|
||||
provider.
|
||||
"""
|
||||
if not secrets_singleton.load_if_toml_exists():
|
||||
raise StreamlitAuthError(
|
||||
"""To use authentication features you need to configure credentials for at
|
||||
least one authentication provider in `.streamlit/secrets.toml`."""
|
||||
)
|
||||
|
||||
auth_section = secrets_singleton.get("auth")
|
||||
if auth_section is None:
|
||||
raise StreamlitAuthError(
|
||||
"""To use authentication features you need to configure credentials for at
|
||||
least one authentication provider in `.streamlit/secrets.toml`."""
|
||||
)
|
||||
if "redirect_uri" not in auth_section:
|
||||
raise StreamlitAuthError(
|
||||
"""Authentication credentials in `.streamlit/secrets.toml` are missing the
|
||||
"redirect_uri" key. Please check your configuration."""
|
||||
)
|
||||
if "cookie_secret" not in auth_section:
|
||||
raise StreamlitAuthError(
|
||||
"""Authentication credentials in `.streamlit/secrets.toml` are missing the
|
||||
"cookie_secret" key. Please check your configuration."""
|
||||
)
|
||||
|
||||
provider_section = auth_section.get(provider)
|
||||
|
||||
# TODO(kajarenc): Revisit this check later when investigating the ability
|
||||
# TODO(kajarenc): to add "_" to the provider name.
|
||||
if "_" in provider:
|
||||
raise StreamlitAuthError(
|
||||
f'Auth provider name "{provider}" contains an underscore. '
|
||||
f"Please use a provider name without underscores."
|
||||
)
|
||||
|
||||
if provider_section is None and provider == "default":
|
||||
provider_section = generate_default_provider_section(auth_section)
|
||||
|
||||
if provider_section is None:
|
||||
if provider == "default":
|
||||
raise StreamlitAuthError(
|
||||
"""Authentication credentials in `.streamlit/secrets.toml` are missing for
|
||||
the default authentication provider. Please check your configuration."""
|
||||
)
|
||||
raise StreamlitAuthError(
|
||||
f"Authentication credentials in `.streamlit/secrets.toml` are missing for "
|
||||
f'the authentication provider "{provider}". Please check your '
|
||||
f"configuration."
|
||||
)
|
||||
|
||||
if not isinstance(provider_section, Mapping):
|
||||
raise StreamlitAuthError(
|
||||
f"Authentication credentials in `.streamlit/secrets.toml` for the "
|
||||
f'authentication provider "{provider}" must be valid TOML. Please check '
|
||||
f"your configuration."
|
||||
)
|
||||
|
||||
required_keys = ["client_id", "client_secret", "server_metadata_url"]
|
||||
missing_keys = [key for key in required_keys if key not in provider_section]
|
||||
if missing_keys:
|
||||
if provider == "default":
|
||||
raise StreamlitAuthError(
|
||||
"Authentication credentials in `.streamlit/secrets.toml` for the "
|
||||
f"default authentication provider are missing the following keys: "
|
||||
f"{missing_keys}. Please check your configuration."
|
||||
)
|
||||
raise StreamlitAuthError(
|
||||
"Authentication credentials in `.streamlit/secrets.toml` for the "
|
||||
f'authentication provider "{provider}" are missing the following keys: '
|
||||
f"{missing_keys}. Please check your configuration."
|
||||
)
|
||||
105
myenv/lib/python3.11/site-packages/streamlit/cli_util.py
Normal file
105
myenv/lib/python3.11/site-packages/streamlit/cli_util.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities related to the CLI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from streamlit import env_util, errors
|
||||
|
||||
|
||||
def print_to_cli(message: str, **kwargs) -> None:
|
||||
"""Print a message to the terminal using click if available, else print
|
||||
using the built-in print function.
|
||||
|
||||
You can provide any keyword arguments that click.secho supports.
|
||||
"""
|
||||
try:
|
||||
import click
|
||||
|
||||
click.secho(message, **kwargs)
|
||||
except ImportError:
|
||||
print(message, flush=True) # noqa: T201
|
||||
|
||||
|
||||
def style_for_cli(message: str, **kwargs) -> str:
|
||||
"""Style a message using click if available, else return the message
|
||||
unchanged.
|
||||
|
||||
You can provide any keyword arguments that click.style supports.
|
||||
"""
|
||||
|
||||
try:
|
||||
import click
|
||||
|
||||
return click.style(message, **kwargs)
|
||||
except ImportError:
|
||||
return message
|
||||
|
||||
|
||||
def _open_browser_with_webbrowser(url: str) -> None:
|
||||
import webbrowser
|
||||
|
||||
webbrowser.open(url)
|
||||
|
||||
|
||||
def _open_browser_with_command(command: str, url: str) -> None:
|
||||
cmd_line = [command, url]
|
||||
with open(os.devnull, "w") as devnull:
|
||||
subprocess.Popen(cmd_line, stdout=devnull, stderr=subprocess.STDOUT)
|
||||
|
||||
|
||||
def open_browser(url: str) -> None:
|
||||
"""Open a web browser pointing to a given URL.
|
||||
|
||||
We use this function instead of Python's `webbrowser` module because this
|
||||
way we can capture stdout/stderr to avoid polluting the terminal with the
|
||||
browser's messages. For example, Chrome always prints things like "Created
|
||||
new window in existing browser session", and those get on the user's way.
|
||||
|
||||
url : str
|
||||
The URL. Must include the protocol.
|
||||
|
||||
"""
|
||||
# Treat Windows separately because:
|
||||
# 1. /dev/null doesn't exist.
|
||||
# 2. subprocess.Popen(['start', url]) doesn't actually pop up the
|
||||
# browser even though 'start url' works from the command prompt.
|
||||
# Fun!
|
||||
# Also, use webbrowser if we are on Linux and xdg-open is not installed.
|
||||
#
|
||||
# We don't use the webbrowser module on Linux and Mac because some browsers
|
||||
# (ahem... Chrome) always print "Opening in existing browser session" to
|
||||
# the terminal, which is spammy and annoying. So instead we start the
|
||||
# browser ourselves and send all its output to /dev/null.
|
||||
|
||||
if env_util.IS_WINDOWS:
|
||||
_open_browser_with_webbrowser(url)
|
||||
return
|
||||
if env_util.IS_LINUX_OR_BSD:
|
||||
if env_util.is_executable_in_path("xdg-open"):
|
||||
_open_browser_with_command("xdg-open", url)
|
||||
return
|
||||
_open_browser_with_webbrowser(url)
|
||||
return
|
||||
if env_util.IS_DARWIN:
|
||||
_open_browser_with_command("open", url)
|
||||
return
|
||||
|
||||
import platform
|
||||
|
||||
raise errors.Error(f'Cannot open browser in platform "{platform.system()}"')
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Column types that can be configured via the ``column_config`` parameter of ``st.dataframe`` and ``st.data_editor``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"AreaChartColumn",
|
||||
"Column",
|
||||
"TextColumn",
|
||||
"NumberColumn",
|
||||
"BarChartColumn",
|
||||
"CheckboxColumn",
|
||||
"DatetimeColumn",
|
||||
"ImageColumn",
|
||||
"SelectboxColumn",
|
||||
"ProgressColumn",
|
||||
"LinkColumn",
|
||||
"LineChartColumn",
|
||||
"ListColumn",
|
||||
"DateColumn",
|
||||
"TimeColumn",
|
||||
"JsonColumn",
|
||||
]
|
||||
|
||||
|
||||
from streamlit.elements.lib.column_types import (
|
||||
AreaChartColumn,
|
||||
BarChartColumn,
|
||||
CheckboxColumn,
|
||||
Column,
|
||||
DateColumn,
|
||||
DatetimeColumn,
|
||||
ImageColumn,
|
||||
JsonColumn,
|
||||
LineChartColumn,
|
||||
LinkColumn,
|
||||
ListColumn,
|
||||
NumberColumn,
|
||||
ProgressColumn,
|
||||
SelectboxColumn,
|
||||
TextColumn,
|
||||
TimeColumn,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
126
myenv/lib/python3.11/site-packages/streamlit/commands/echo.py
Normal file
126
myenv/lib/python3.11/site-packages/streamlit/commands/echo.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import contextlib
|
||||
import re
|
||||
import textwrap
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
_SPACES_RE = re.compile("\\s*")
|
||||
_EMPTY_LINE_RE = re.compile("\\s*\n")
|
||||
|
||||
|
||||
@gather_metrics("echo")
|
||||
@contextlib.contextmanager
|
||||
def echo(code_location="above"):
|
||||
"""Use in a `with` block to draw some code on the app, then execute it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
code_location : "above" or "below"
|
||||
Whether to show the echoed code before or after the results of the
|
||||
executed code block.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.echo():
|
||||
>>> st.write('This code will be printed')
|
||||
|
||||
"""
|
||||
from streamlit import code, empty, source_util, warning
|
||||
|
||||
if code_location == "below":
|
||||
show_code = code
|
||||
show_warning = warning
|
||||
else:
|
||||
placeholder = empty()
|
||||
show_code = placeholder.code
|
||||
show_warning = placeholder.warning
|
||||
|
||||
try:
|
||||
# Get stack frame *before* running the echoed code. The frame's
|
||||
# line number will point to the `st.echo` statement we're running.
|
||||
frame = traceback.extract_stack()[-3]
|
||||
filename, start_line = frame.filename, frame.lineno or 0
|
||||
|
||||
# Read the file containing the source code of the echoed statement.
|
||||
with source_util.open_python_file(filename) as source_file:
|
||||
source_lines = source_file.readlines()
|
||||
|
||||
# Use ast to parse the Python file and find the code block to display
|
||||
root_node = ast.parse("".join(source_lines))
|
||||
line_to_node_map: dict[int, Any] = {}
|
||||
|
||||
def collect_body_statements(node: ast.AST) -> None:
|
||||
if not hasattr(node, "body"):
|
||||
return
|
||||
for child in ast.iter_child_nodes(node):
|
||||
# If child doesn't have "lineno", it is not something we could display
|
||||
if hasattr(child, "lineno"):
|
||||
line_to_node_map[child.lineno] = child
|
||||
collect_body_statements(child)
|
||||
|
||||
collect_body_statements(root_node)
|
||||
|
||||
# In AST module the lineno (line numbers) are 1-indexed,
|
||||
# so we decrease it by 1 to lookup in source lines list
|
||||
echo_block_start_line = line_to_node_map[start_line].body[0].lineno - 1
|
||||
echo_block_end_line = line_to_node_map[start_line].end_lineno
|
||||
lines_to_display = source_lines[echo_block_start_line:echo_block_end_line]
|
||||
|
||||
code_string = textwrap.dedent("".join(lines_to_display))
|
||||
|
||||
# Run the echoed code...
|
||||
yield
|
||||
|
||||
# And draw the code string to the app!
|
||||
show_code(code_string, "python")
|
||||
|
||||
except FileNotFoundError as err:
|
||||
show_warning("Unable to display code. %s" % err)
|
||||
|
||||
|
||||
def _get_initial_indent(lines: Iterable[str]) -> int:
|
||||
"""Return the indent of the first non-empty line in the list.
|
||||
If all lines are empty, return 0.
|
||||
"""
|
||||
for line in lines:
|
||||
indent = _get_indent(line)
|
||||
if indent is not None:
|
||||
return indent
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _get_indent(line: str) -> int | None:
|
||||
"""Get the number of whitespaces at the beginning of the given line.
|
||||
If the line is empty, or if it contains just whitespace and a newline,
|
||||
return None.
|
||||
"""
|
||||
if _EMPTY_LINE_RE.match(line) is not None:
|
||||
return None
|
||||
|
||||
match = _SPACES_RE.match(line)
|
||||
return match.end() if match is not None else 0
|
||||
@@ -0,0 +1,238 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from itertools import dropwhile
|
||||
from pathlib import Path
|
||||
from typing import Literal, NoReturn
|
||||
|
||||
import streamlit as st
|
||||
from streamlit.errors import NoSessionContext, StreamlitAPIException
|
||||
from streamlit.file_util import get_main_script_directory, normalize_path_join
|
||||
from streamlit.navigation.page import StreamlitPage
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import (
|
||||
RerunData,
|
||||
ScriptRunContext,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
|
||||
|
||||
@gather_metrics("stop")
|
||||
def stop() -> NoReturn: # type: ignore[misc]
|
||||
"""Stops execution immediately.
|
||||
|
||||
Streamlit will not run any statements after `st.stop()`.
|
||||
We recommend rendering a message to explain why the script has stopped.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> name = st.text_input("Name")
|
||||
>>> if not name:
|
||||
>>> st.warning('Please input a name.')
|
||||
>>> st.stop()
|
||||
>>> st.success("Thank you for inputting a name.")
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if ctx and ctx.script_requests:
|
||||
ctx.script_requests.request_stop()
|
||||
# Force a yield point so the runner can stop
|
||||
st.empty()
|
||||
|
||||
|
||||
def _new_fragment_id_queue(
|
||||
ctx: ScriptRunContext,
|
||||
scope: Literal["app", "fragment"],
|
||||
) -> list[str]:
|
||||
if scope == "app":
|
||||
return []
|
||||
|
||||
else: # scope == "fragment"
|
||||
curr_queue = ctx.fragment_ids_this_run
|
||||
|
||||
# If st.rerun(scope="fragment") is called during a full script run, we raise an
|
||||
# exception. This occurs, of course, if st.rerun(scope="fragment") is called
|
||||
# outside of a fragment, but it somewhat surprisingly occurs if it gets called
|
||||
# from within a fragment during a run of the full script. While this behvior may
|
||||
# be surprising, it seems somewhat reasonable given that the correct behavior of
|
||||
# calling st.rerun(scope="fragment") in this situation is unclear to me:
|
||||
# * Rerunning just the fragment immediately may cause weirdness down the line
|
||||
# as any part of the script that occurs after the fragment will not be
|
||||
# executed.
|
||||
# * Waiting until the full script run completes before rerunning the fragment
|
||||
# seems odd (even if we normally do this before running a fragment not
|
||||
# triggered by st.rerun()) because it defers the execution of st.rerun().
|
||||
# * Rerunning the full app feels incorrect as we're seemingly ignoring the
|
||||
# `scope` argument.
|
||||
# With these issues and given that it seems pretty unnatural to have a
|
||||
# fragment-scoped rerun happen during a full script run to begin with, it seems
|
||||
# reasonable to just disallow this completely for now.
|
||||
if not curr_queue:
|
||||
raise StreamlitAPIException(
|
||||
'scope="fragment" can only be specified from `@st.fragment`-decorated '
|
||||
"functions during fragment reruns."
|
||||
)
|
||||
|
||||
assert (
|
||||
new_queue := list(
|
||||
dropwhile(lambda x: x != ctx.current_fragment_id, curr_queue)
|
||||
)
|
||||
), (
|
||||
"Could not find current_fragment_id in fragment_id_queue. This should never happen."
|
||||
)
|
||||
|
||||
return new_queue
|
||||
|
||||
|
||||
@gather_metrics("rerun")
|
||||
def rerun( # type: ignore[misc]
|
||||
*, # The scope argument can only be passed via keyword.
|
||||
scope: Literal["app", "fragment"] = "app",
|
||||
) -> NoReturn:
|
||||
"""Rerun the script immediately.
|
||||
|
||||
When ``st.rerun()`` is called, Streamlit halts the current script run and
|
||||
executes no further statements. Streamlit immediately queues the script to
|
||||
rerun.
|
||||
|
||||
When using ``st.rerun`` in a fragment, you can scope the rerun to the
|
||||
fragment. However, if a fragment is running as part of a full-app rerun,
|
||||
a fragment-scoped rerun is not allowed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
scope : "app" or "fragment"
|
||||
Specifies what part of the app should rerun. If ``scope`` is ``"app"``
|
||||
(default), the full app reruns. If ``scope`` is ``"fragment"``,
|
||||
Streamlit only reruns the fragment from which this command is called.
|
||||
|
||||
Setting ``scope="fragment"`` is only valid inside a fragment during a
|
||||
fragment rerun. If ``st.rerun(scope="fragment")`` is called during a
|
||||
full-app rerun or outside of a fragment, Streamlit will raise a
|
||||
``StreamlitAPIException``.
|
||||
|
||||
"""
|
||||
|
||||
if scope not in ["app", "fragment"]:
|
||||
raise StreamlitAPIException(
|
||||
f"'{scope}'is not a valid rerun scope. Valid scopes are 'app' and 'fragment'."
|
||||
)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if ctx and ctx.script_requests:
|
||||
query_string = ctx.query_string
|
||||
page_script_hash = ctx.page_script_hash
|
||||
|
||||
ctx.script_requests.request_rerun(
|
||||
RerunData(
|
||||
query_string=query_string,
|
||||
page_script_hash=page_script_hash,
|
||||
fragment_id_queue=_new_fragment_id_queue(ctx, scope),
|
||||
is_fragment_scoped_rerun=scope == "fragment",
|
||||
)
|
||||
)
|
||||
# Force a yield point so the runner can do the rerun
|
||||
st.empty()
|
||||
|
||||
|
||||
@gather_metrics("switch_page")
|
||||
def switch_page(page: str | Path | StreamlitPage) -> NoReturn: # type: ignore[misc]
|
||||
"""Programmatically switch the current page in a multipage app.
|
||||
|
||||
When ``st.switch_page`` is called, the current page execution stops and
|
||||
the specified page runs as if the user clicked on it in the sidebar
|
||||
navigation. The specified page must be recognized by Streamlit's multipage
|
||||
architecture (your main Python file or a Python file in a ``pages/``
|
||||
folder). Arbitrary Python scripts cannot be passed to ``st.switch_page``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
page: str, Path, or st.Page
|
||||
The file path (relative to the main script) or an st.Page indicating
|
||||
the page to switch to.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
Consider the following example given this file structure:
|
||||
|
||||
>>> your-repository/
|
||||
>>> ├── pages/
|
||||
>>> │ ├── page_1.py
|
||||
>>> │ └── page_2.py
|
||||
>>> └── your_app.py
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> if st.button("Home"):
|
||||
>>> st.switch_page("your_app.py")
|
||||
>>> if st.button("Page 1"):
|
||||
>>> st.switch_page("pages/page_1.py")
|
||||
>>> if st.button("Page 2"):
|
||||
>>> st.switch_page("pages/page_2.py")
|
||||
|
||||
.. output ::
|
||||
https://doc-switch-page.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if not ctx or not ctx.script_requests:
|
||||
# This should never be the case
|
||||
raise NoSessionContext()
|
||||
|
||||
page_script_hash = ""
|
||||
if isinstance(page, StreamlitPage):
|
||||
page_script_hash = page._script_hash
|
||||
else:
|
||||
# Convert Path to string if necessary
|
||||
if isinstance(page, Path):
|
||||
page = str(page)
|
||||
|
||||
main_script_directory = get_main_script_directory(ctx.main_script_path)
|
||||
requested_page = os.path.realpath(
|
||||
normalize_path_join(main_script_directory, page)
|
||||
)
|
||||
all_app_pages = ctx.pages_manager.get_pages().values()
|
||||
|
||||
matched_pages = [p for p in all_app_pages if p["script_path"] == requested_page]
|
||||
|
||||
if len(matched_pages) == 0:
|
||||
raise StreamlitAPIException(
|
||||
f"Could not find page: `{page}`. Must be the file path relative to the main script, from the directory: `{os.path.basename(main_script_directory)}`. Only the main app file and files in the `pages/` directory are supported."
|
||||
)
|
||||
|
||||
page_script_hash = matched_pages[0]["page_script_hash"]
|
||||
|
||||
# We want to reset query params (with exception of embed) when switching pages
|
||||
with ctx.session_state.query_params() as qp:
|
||||
qp.clear()
|
||||
|
||||
ctx.script_requests.request_rerun(
|
||||
RerunData(
|
||||
query_string=ctx.query_string,
|
||||
page_script_hash=page_script_hash,
|
||||
)
|
||||
)
|
||||
# Force a yield point so the runner can do the rerun
|
||||
st.empty()
|
||||
@@ -0,0 +1,169 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import urllib.parse as parse
|
||||
from typing import Any
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.runtime.state.query_params import (
|
||||
EMBED_OPTIONS_QUERY_PARAM,
|
||||
EMBED_QUERY_PARAM,
|
||||
EMBED_QUERY_PARAMS_KEYS,
|
||||
)
|
||||
|
||||
|
||||
@gather_metrics("experimental_get_query_params")
|
||||
def get_query_params() -> dict[str, list[str]]:
|
||||
"""Return the query parameters that is currently showing in the browser's URL bar.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The current query parameters as a dict. "Query parameters" are the part of the URL that comes
|
||||
after the first "?".
|
||||
|
||||
Example
|
||||
-------
|
||||
Let's say the user's web browser is at
|
||||
`http://localhost:8501/?show_map=True&selected=asia&selected=america`.
|
||||
Then, you can get the query parameters using the following:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.experimental_get_query_params()
|
||||
{"show_map": ["True"], "selected": ["asia", "america"]}
|
||||
|
||||
Note that the values in the returned dict are *always* lists. This is
|
||||
because we internally use Python's urllib.parse.parse_qs(), which behaves
|
||||
this way. And this behavior makes sense when you consider that every item
|
||||
in a query string is potentially a 1-element array.
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is None:
|
||||
return {}
|
||||
ctx.mark_experimental_query_params_used()
|
||||
# Return new query params dict, but without embed, embed_options query params
|
||||
return _exclude_keys_in_dict(
|
||||
parse.parse_qs(ctx.query_string, keep_blank_values=True),
|
||||
keys_to_exclude=EMBED_QUERY_PARAMS_KEYS,
|
||||
)
|
||||
|
||||
|
||||
@gather_metrics("experimental_set_query_params")
|
||||
def set_query_params(**query_params: Any) -> None:
|
||||
"""Set the query parameters that are shown in the browser's URL bar.
|
||||
|
||||
.. warning::
|
||||
Query param `embed` cannot be set using this method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**query_params : dict
|
||||
The query parameters to set, as key-value pairs.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
To point the user's web browser to something like
|
||||
"http://localhost:8501/?show_map=True&selected=asia&selected=america",
|
||||
you would do the following:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.experimental_set_query_params(
|
||||
... show_map=True,
|
||||
... selected=["asia", "america"],
|
||||
... )
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is None:
|
||||
return
|
||||
ctx.mark_experimental_query_params_used()
|
||||
|
||||
msg = ForwardMsg()
|
||||
msg.page_info_changed.query_string = _ensure_no_embed_params(
|
||||
query_params, ctx.query_string
|
||||
)
|
||||
ctx.query_string = msg.page_info_changed.query_string
|
||||
ctx.enqueue(msg)
|
||||
|
||||
|
||||
def _exclude_keys_in_dict(
|
||||
d: dict[str, Any], keys_to_exclude: list[str]
|
||||
) -> dict[str, Any]:
|
||||
"""Returns new object but without keys defined in keys_to_exclude."""
|
||||
return {
|
||||
key: value for key, value in d.items() if key.lower() not in keys_to_exclude
|
||||
}
|
||||
|
||||
|
||||
def _extract_key_query_params(
|
||||
query_params: dict[str, list[str]], param_key: str
|
||||
) -> set[str]:
|
||||
"""Extracts key (case-insensitive) query params from Dict, and returns them as Set of str."""
|
||||
return {
|
||||
item.lower()
|
||||
for sublist in [
|
||||
[value.lower() for value in query_params[key]]
|
||||
for key in query_params.keys()
|
||||
if key.lower() == param_key and query_params.get(key)
|
||||
]
|
||||
for item in sublist
|
||||
}
|
||||
|
||||
|
||||
def _ensure_no_embed_params(
|
||||
query_params: dict[str, list[str] | str], query_string: str
|
||||
) -> str:
|
||||
"""Ensures there are no embed params set (raises StreamlitAPIException) if there is a try,
|
||||
also makes sure old param values in query_string are preserved. Returns query_string : str.
|
||||
"""
|
||||
# Get query params dict without embed, embed_options params
|
||||
query_params_without_embed = _exclude_keys_in_dict(
|
||||
query_params, keys_to_exclude=EMBED_QUERY_PARAMS_KEYS
|
||||
)
|
||||
if query_params != query_params_without_embed:
|
||||
raise StreamlitAPIException(
|
||||
"Query param embed and embed_options (case-insensitive) cannot be set using set_query_params method."
|
||||
)
|
||||
|
||||
all_current_params = parse.parse_qs(query_string, keep_blank_values=True)
|
||||
current_embed_params = parse.urlencode(
|
||||
{
|
||||
EMBED_QUERY_PARAM: list(
|
||||
_extract_key_query_params(
|
||||
all_current_params, param_key=EMBED_QUERY_PARAM
|
||||
)
|
||||
),
|
||||
EMBED_OPTIONS_QUERY_PARAM: list(
|
||||
_extract_key_query_params(
|
||||
all_current_params, param_key=EMBED_OPTIONS_QUERY_PARAM
|
||||
)
|
||||
),
|
||||
},
|
||||
doseq=True,
|
||||
)
|
||||
query_string = parse.urlencode(query_params, doseq=True)
|
||||
|
||||
if query_string:
|
||||
separator = "&" if current_embed_params else ""
|
||||
return separator.join([query_string, current_embed_params])
|
||||
return current_embed_params
|
||||
189
myenv/lib/python3.11/site-packages/streamlit/commands/logo.py
Normal file
189
myenv/lib/python3.11/site-packages/streamlit/commands/logo.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Handle App logos."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from streamlit import url_util
|
||||
from streamlit.elements.lib.image_utils import AtomicImage, WidthBehavior, image_to_url
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
|
||||
|
||||
def _invalid_logo_text(field_name: str):
|
||||
return f"The {field_name} passed to st.logo is invalid - See [documentation](https://docs.streamlit.io/develop/api-reference/media/st.logo) for more information on valid types"
|
||||
|
||||
|
||||
@gather_metrics("logo")
|
||||
def logo(
|
||||
image: AtomicImage,
|
||||
*, # keyword-only args:
|
||||
size: Literal["small", "medium", "large"] = "medium",
|
||||
link: str | None = None,
|
||||
icon_image: AtomicImage | None = None,
|
||||
) -> None:
|
||||
r"""
|
||||
Renders a logo in the upper-left corner of your app and its sidebar.
|
||||
|
||||
If ``st.logo`` is called multiple times within a page, Streamlit will
|
||||
render the image passed in the last call. For the most consistent results,
|
||||
call ``st.logo`` early in your page script and choose an image that works
|
||||
well in both light and dark mode. Avoid empty margins around your image.
|
||||
|
||||
If your logo does not work well for both light and dark mode, consider
|
||||
setting the theme and hiding the settings menu from users with the
|
||||
`configuration option <https://docs.streamlit.io/develop/api-reference/configuration/config.toml>`_
|
||||
``client.toolbarMode="minimal"``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image: Anything supported by st.image (except list)
|
||||
The image to display in the upper-left corner of your app and its
|
||||
sidebar. This can be any of the types supported by |st.image|_ except
|
||||
a list. If ``icon_image`` is also provided, then Streamlit will only
|
||||
display ``image`` in the sidebar.
|
||||
|
||||
Streamlit scales the image to a max height set by ``size`` and a max
|
||||
width to fit within the sidebar.
|
||||
|
||||
.. |st.image| replace:: ``st.image``
|
||||
.. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image
|
||||
|
||||
size: "small", "medium", or "large"
|
||||
The size of the image displayed in the upper-left corner of the app and its
|
||||
sidebar. The possible values are as follows:
|
||||
|
||||
- ``"small"``: 20px max height
|
||||
- ``"medium"`` (default): 24px max height
|
||||
- ``"large"``: 32px max height
|
||||
|
||||
link : str or None
|
||||
The external URL to open when a user clicks on the logo. The URL must
|
||||
start with "\http://" or "\https://". If ``link`` is ``None`` (default),
|
||||
the logo will not include a hyperlink.
|
||||
icon_image: Anything supported by st.image (except list) or None
|
||||
An optional, typically smaller image to replace ``image`` in the
|
||||
upper-left corner when the sidebar is closed. This can be any of the
|
||||
types supported by ``st.image`` except a list. If ``icon_image`` is
|
||||
``None`` (default), Streamlit will always display ``image`` in the
|
||||
upper-left corner, regardless of whether the sidebar is open or closed.
|
||||
Otherwise, Streamlit will render ``icon_image`` in the upper-left
|
||||
corner of the app when the sidebar is closed.
|
||||
|
||||
Streamlit scales the image to a max height set by ``size`` and a max
|
||||
width to fit within the sidebar. If the sidebar is closed, the max
|
||||
width is retained from when it was last open.
|
||||
|
||||
For best results, pass a wide or horizontal image to ``image`` and a
|
||||
square image to ``icon_image``. Or, pass a square image to ``image``
|
||||
and leave ``icon_image=None``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
A common design practice is to use a wider logo in the sidebar, and a
|
||||
smaller, icon-styled logo in your app's main body.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.logo(
|
||||
... LOGO_URL_LARGE,
|
||||
... link="https://streamlit.io/gallery",
|
||||
... icon_image=LOGO_URL_SMALL,
|
||||
... )
|
||||
|
||||
Try switching logos around in the following example:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> HORIZONTAL_RED = "images/horizontal_red.png"
|
||||
>>> ICON_RED = "images/icon_red.png"
|
||||
>>> HORIZONTAL_BLUE = "images/horizontal_blue.png"
|
||||
>>> ICON_BLUE = "images/icon_blue.png"
|
||||
>>>
|
||||
>>> options = [HORIZONTAL_RED, ICON_RED, HORIZONTAL_BLUE, ICON_BLUE]
|
||||
>>> sidebar_logo = st.selectbox("Sidebar logo", options, 0)
|
||||
>>> main_body_logo = st.selectbox("Main body logo", options, 1)
|
||||
>>>
|
||||
>>> st.logo(sidebar_logo, icon_image=main_body_logo)
|
||||
>>> st.sidebar.markdown("Hi!")
|
||||
|
||||
.. output::
|
||||
https://doc-logo.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
"""
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is None:
|
||||
return
|
||||
|
||||
fwd_msg = ForwardMsg()
|
||||
|
||||
try:
|
||||
image_url = image_to_url(
|
||||
image,
|
||||
width=WidthBehavior.AUTO,
|
||||
clamp=False,
|
||||
channels="RGB",
|
||||
output_format="auto",
|
||||
image_id="logo",
|
||||
)
|
||||
fwd_msg.logo.image = image_url
|
||||
except Exception as ex:
|
||||
raise StreamlitAPIException(_invalid_logo_text("image")) from ex
|
||||
|
||||
if link:
|
||||
# Handle external links:
|
||||
if url_util.is_url(link, ("http", "https")):
|
||||
fwd_msg.logo.link = link
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid link: {link} - the link param supports external links only and must start with either http:// or https://."
|
||||
)
|
||||
|
||||
if icon_image:
|
||||
try:
|
||||
icon_image_url = image_to_url(
|
||||
icon_image,
|
||||
width=WidthBehavior.AUTO,
|
||||
clamp=False,
|
||||
channels="RGB",
|
||||
output_format="auto",
|
||||
image_id="icon-image",
|
||||
)
|
||||
fwd_msg.logo.icon_image = icon_image_url
|
||||
except Exception as ex:
|
||||
raise StreamlitAPIException(_invalid_logo_text("icon_image")) from ex
|
||||
|
||||
def validate_size(size):
|
||||
if isinstance(size, str):
|
||||
image_size = size.lower()
|
||||
valid_sizes = ["small", "medium", "large"]
|
||||
|
||||
if image_size in valid_sizes:
|
||||
return image_size
|
||||
|
||||
raise StreamlitAPIException(
|
||||
f'The size argument to st.logo must be "small", "medium", or "large". \n'
|
||||
f"The argument passed was {size}."
|
||||
)
|
||||
|
||||
fwd_msg.logo.size = validate_size(size)
|
||||
|
||||
ctx.enqueue(fwd_msg)
|
||||
@@ -0,0 +1,385 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Callable, Literal, Union
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.navigation.page import StreamlitPage
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.proto.Navigation_pb2 import Navigation as NavigationProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.pages_manager import PagesManager
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
ScriptRunContext,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.source_util import PageHash, PageInfo
|
||||
|
||||
SectionHeader: TypeAlias = str
|
||||
PageType: TypeAlias = Union[str, Path, Callable[[], None], StreamlitPage]
|
||||
|
||||
|
||||
def convert_to_streamlit_page(
|
||||
page_input: PageType,
|
||||
) -> StreamlitPage:
|
||||
"""Convert various input types to StreamlitPage objects."""
|
||||
if isinstance(page_input, StreamlitPage):
|
||||
return page_input
|
||||
|
||||
if isinstance(page_input, str):
|
||||
return StreamlitPage(page_input)
|
||||
|
||||
if isinstance(page_input, Path):
|
||||
return StreamlitPage(page_input)
|
||||
|
||||
if callable(page_input):
|
||||
# Convert function to StreamlitPage
|
||||
return StreamlitPage(page_input)
|
||||
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid page type: {type(page_input)}. Must be either a string path, "
|
||||
"a pathlib.Path, a callable function, or a st.Page object."
|
||||
)
|
||||
|
||||
|
||||
def pages_from_nav_sections(
|
||||
nav_sections: dict[SectionHeader, list[StreamlitPage]],
|
||||
) -> list[StreamlitPage]:
|
||||
page_list = []
|
||||
for pages in nav_sections.values():
|
||||
for page in pages:
|
||||
page_list.append(page)
|
||||
|
||||
return page_list
|
||||
|
||||
|
||||
def send_page_not_found(ctx: ScriptRunContext):
|
||||
msg = ForwardMsg()
|
||||
msg.page_not_found.page_name = ""
|
||||
ctx.enqueue(msg)
|
||||
|
||||
|
||||
@gather_metrics("navigation")
|
||||
def navigation(
|
||||
pages: Sequence[PageType] | Mapping[SectionHeader, Sequence[PageType]],
|
||||
*,
|
||||
position: Literal["sidebar", "hidden"] = "sidebar",
|
||||
expanded: bool = False,
|
||||
) -> StreamlitPage:
|
||||
"""
|
||||
Configure the available pages in a multipage app.
|
||||
|
||||
Call ``st.navigation`` in your entrypoint file to define the available
|
||||
pages for your app. ``st.navigation`` returns the current page, which can
|
||||
be executed using ``.run()`` method.
|
||||
|
||||
When using ``st.navigation``, your entrypoint file (the file passed to
|
||||
``streamlit run``) acts like a router or frame of common elements around
|
||||
each of your pages. Streamlit executes the entrypoint file with every app
|
||||
rerun. To execute the current page, you must call the ``.run()`` method on
|
||||
the ``StreamlitPage`` object returned by ``st.navigation``.
|
||||
|
||||
The set of available pages can be updated with each rerun for dynamic
|
||||
navigation. By default, ``st.navigation`` displays the available pages in
|
||||
the sidebar if there is more than one page. This behavior can be changed
|
||||
using the ``position`` keyword argument.
|
||||
|
||||
As soon as any session of your app executes the ``st.navigation`` command,
|
||||
your app will ignore the ``pages/`` directory (across all sessions).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pages : Sequence[page-like], Mapping[str, Sequence[page-like]]
|
||||
The available pages for the app.
|
||||
|
||||
To create a navigation menu with no sections or page groupings,
|
||||
``pages`` must be a list of page-like objects. Page-like objects are
|
||||
anything that can be passed to ``st.Page`` or a ``StreamlitPage``
|
||||
object returned by ``st.Page``.
|
||||
|
||||
To create labeled sections or page groupings within the navigation
|
||||
menu, ``pages`` must be a dictionary. Each key is the label of a
|
||||
section and each value is the list of page-like objects for
|
||||
that section.
|
||||
|
||||
When you use a string or path as a page-like object, they are
|
||||
internally passed to ``st.Page`` and converted to ``StreamlitPage``
|
||||
objects. In this case, the page will have the default title, icon, and
|
||||
path inferred from its path or filename. To customize these attributes
|
||||
for your page, initialize your page with ``st.Page``.
|
||||
|
||||
position : "sidebar" or "hidden"
|
||||
The position of the navigation menu. If this is ``"sidebar"``
|
||||
(default), the navigation widget appears at the top of the sidebar. If
|
||||
this is ``"hidden"``, the navigation widget is not displayed.
|
||||
|
||||
If there is only one page in ``pages``, the navigation will be hidden
|
||||
for any value of ``position``.
|
||||
|
||||
expanded : bool
|
||||
Whether the navigation menu should be expanded. If this is ``False``
|
||||
(default), the navigation menu will be collapsed and will include a
|
||||
button to view more options when there are too many pages to display.
|
||||
If this is ``True``, the navigation menu will always be expanded; no
|
||||
button to collapse the menu will be displayed.
|
||||
|
||||
If ``st.navigation`` changes from ``expanded=True`` to
|
||||
``expanded=False`` on a rerun, the menu will stay expanded and a
|
||||
collapse button will be displayed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
StreamlitPage
|
||||
The current page selected by the user. To run the page, you must use
|
||||
the ``.run()`` method on it.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following examples show different possible entrypoint files, each named
|
||||
``streamlit_app.py``. An entrypoint file is passed to ``streamlit run``. It
|
||||
manages your app's navigation and serves as a router between pages.
|
||||
|
||||
**Example 1: Use a callable or Python file as a page**
|
||||
|
||||
You can declare pages from callables or file paths. If you pass callables
|
||||
or paths to ``st.navigation`` as a page-like objects, they are internally
|
||||
converted to ``StreamlitPage`` objects using ``st.Page``. In this case, the
|
||||
page titles, icons, and paths are inferred from the file or callable names.
|
||||
|
||||
``page_1.py`` (in the same directory as your entrypoint file):
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.title("Page 1")
|
||||
|
||||
``streamlit_app.py``:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> def page_2():
|
||||
... st.title("Page 2")
|
||||
>>>
|
||||
>>> pg = st.navigation(["page_1.py", page_2])
|
||||
>>> pg.run()
|
||||
|
||||
.. output::
|
||||
https://doc-navigation-example-1.streamlit.app/
|
||||
height: 200px
|
||||
|
||||
**Example 2: Group pages into sections and customize them with ``st.Page``**
|
||||
|
||||
You can use a dictionary to create sections within your navigation menu. In
|
||||
the following example, each page is similar to Page 1 in Example 1, and all
|
||||
pages are in the same directory. However, you can use Python files from
|
||||
anywhere in your repository. ``st.Page`` is used to give each page a custom
|
||||
title. For more information, see |st.Page|_.
|
||||
|
||||
Directory structure:
|
||||
|
||||
>>> your_repository/
|
||||
>>> ├── create_account.py
|
||||
>>> ├── learn.py
|
||||
>>> ├── manage_account.py
|
||||
>>> ├── streamlit_app.py
|
||||
>>> └── trial.py
|
||||
|
||||
``streamlit_app.py``:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> pages = {
|
||||
... "Your account": [
|
||||
... st.Page("create_account.py", title="Create your account"),
|
||||
... st.Page("manage_account.py", title="Manage your account"),
|
||||
... ],
|
||||
... "Resources": [
|
||||
... st.Page("learn.py", title="Learn about us"),
|
||||
... st.Page("trial.py", title="Try it out"),
|
||||
... ],
|
||||
... }
|
||||
>>>
|
||||
>>> pg = st.navigation(pages)
|
||||
>>> pg.run()
|
||||
|
||||
.. output::
|
||||
https://doc-navigation-example-2.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
**Example 3: Stateful widgets across multiple pages**
|
||||
|
||||
Call widget functions in your entrypoint file when you want a widget to be
|
||||
stateful across pages. Assign keys to your common widgets and access their
|
||||
values through Session State within your pages.
|
||||
|
||||
``streamlit_app.py``:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> def page1():
|
||||
>>> st.write(st.session_state.foo)
|
||||
>>>
|
||||
>>> def page2():
|
||||
>>> st.write(st.session_state.bar)
|
||||
>>>
|
||||
>>> # Widgets shared by all the pages
|
||||
>>> st.sidebar.selectbox("Foo", ["A", "B", "C"], key="foo")
|
||||
>>> st.sidebar.checkbox("Bar", key="bar")
|
||||
>>>
|
||||
>>> pg = st.navigation([page1, page2])
|
||||
>>> pg.run()
|
||||
|
||||
.. output::
|
||||
https://doc-navigation-multipage-widgets.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
.. |st.Page| replace:: ``st.Page``
|
||||
.. _st.Page: https://docs.streamlit.io/develop/api-reference/navigation/st.page
|
||||
|
||||
"""
|
||||
# Disable the use of the pages feature (ie disregard v1 behavior of Multipage Apps)
|
||||
PagesManager.uses_pages_directory = False
|
||||
|
||||
return _navigation(pages, position=position, expanded=expanded)
|
||||
|
||||
|
||||
def _navigation(
|
||||
pages: Sequence[PageType] | Mapping[SectionHeader, Sequence[PageType]],
|
||||
*,
|
||||
position: Literal["sidebar", "hidden"],
|
||||
expanded: bool,
|
||||
) -> StreamlitPage:
|
||||
if isinstance(pages, Sequence):
|
||||
converted_pages = [convert_to_streamlit_page(p) for p in pages]
|
||||
nav_sections = {"": converted_pages}
|
||||
else:
|
||||
nav_sections = {
|
||||
section: [convert_to_streamlit_page(p) for p in section_pages]
|
||||
for section, section_pages in pages.items()
|
||||
}
|
||||
page_list = pages_from_nav_sections(nav_sections)
|
||||
|
||||
if not page_list:
|
||||
raise StreamlitAPIException(
|
||||
"`st.navigation` must be called with at least one `st.Page`."
|
||||
)
|
||||
|
||||
default_page = None
|
||||
pagehash_to_pageinfo: dict[PageHash, PageInfo] = {}
|
||||
|
||||
# Get the default page.
|
||||
for section_header in nav_sections:
|
||||
for page in nav_sections[section_header]:
|
||||
if page._default:
|
||||
if default_page is not None:
|
||||
raise StreamlitAPIException(
|
||||
"Multiple Pages specified with `default=True`. "
|
||||
"At most one Page can be set to default."
|
||||
)
|
||||
default_page = page
|
||||
|
||||
if default_page is None:
|
||||
default_page = page_list[0]
|
||||
default_page._default = True
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if not ctx:
|
||||
# This should never run in Streamlit, but we want to make sure that
|
||||
# the function always returns a page
|
||||
default_page._can_be_called = True
|
||||
return default_page
|
||||
|
||||
# Build the pagehash-to-pageinfo mapping.
|
||||
for section_header in nav_sections:
|
||||
for page in nav_sections[section_header]:
|
||||
if isinstance(page._page, Path):
|
||||
script_path = str(page._page)
|
||||
else:
|
||||
script_path = ""
|
||||
|
||||
script_hash = page._script_hash
|
||||
if script_hash in pagehash_to_pageinfo:
|
||||
# The page script hash is soley based on the url path
|
||||
# So duplicate page script hashes are due to duplicate url paths
|
||||
raise StreamlitAPIException(
|
||||
f"Multiple Pages specified with URL pathname {page.url_path}. "
|
||||
"URL pathnames must be unique. The url pathname may be "
|
||||
"inferred from the filename, callable name, or title."
|
||||
)
|
||||
|
||||
pagehash_to_pageinfo[script_hash] = {
|
||||
"page_script_hash": script_hash,
|
||||
"page_name": page.title,
|
||||
"icon": page.icon,
|
||||
"script_path": script_path,
|
||||
"url_pathname": page.url_path,
|
||||
}
|
||||
|
||||
msg = ForwardMsg()
|
||||
if position == "hidden":
|
||||
msg.navigation.position = NavigationProto.Position.HIDDEN
|
||||
elif config.get_option("client.showSidebarNavigation") is False:
|
||||
msg.navigation.position = NavigationProto.Position.HIDDEN
|
||||
else:
|
||||
msg.navigation.position = NavigationProto.Position.SIDEBAR
|
||||
|
||||
msg.navigation.expanded = expanded
|
||||
msg.navigation.sections[:] = nav_sections.keys()
|
||||
for section_header in nav_sections:
|
||||
for page in nav_sections[section_header]:
|
||||
p = msg.navigation.app_pages.add()
|
||||
p.page_script_hash = page._script_hash
|
||||
p.page_name = page.title
|
||||
p.icon = page.icon
|
||||
p.is_default = page._default
|
||||
p.section_header = section_header
|
||||
p.url_pathname = page.url_path
|
||||
|
||||
# Inform our page manager about the set of pages we have
|
||||
ctx.pages_manager.set_pages(pagehash_to_pageinfo)
|
||||
found_page = ctx.pages_manager.get_page_script(
|
||||
fallback_page_hash=default_page._script_hash
|
||||
)
|
||||
|
||||
page_to_return = None
|
||||
if found_page:
|
||||
found_page_script_hash = found_page["page_script_hash"]
|
||||
matching_pages = [
|
||||
p for p in page_list if p._script_hash == found_page_script_hash
|
||||
]
|
||||
if len(matching_pages) > 0:
|
||||
page_to_return = matching_pages[0]
|
||||
|
||||
if not page_to_return:
|
||||
send_page_not_found(ctx)
|
||||
page_to_return = default_page
|
||||
|
||||
# Ordain the page that can be called
|
||||
page_to_return._can_be_called = True
|
||||
msg.navigation.page_script_hash = page_to_return._script_hash
|
||||
# Set the current page script hash to the page that is going to be executed
|
||||
ctx.set_mpa_v2_page(page_to_return._script_hash)
|
||||
|
||||
# This will either navigation or yield if the page is not found
|
||||
ctx.enqueue(msg)
|
||||
|
||||
return page_to_return
|
||||
@@ -0,0 +1,311 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.elements.lib.image_utils import AtomicImage, image_to_url
|
||||
from streamlit.errors import (
|
||||
StreamlitInvalidMenuItemKeyError,
|
||||
StreamlitInvalidPageLayoutError,
|
||||
StreamlitInvalidSidebarStateError,
|
||||
StreamlitInvalidURLError,
|
||||
)
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg as ForwardProto
|
||||
from streamlit.proto.PageConfig_pb2 import PageConfig as PageConfigProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.string_util import is_emoji, validate_material_icon
|
||||
from streamlit.url_util import is_url
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
GET_HELP_KEY: Final = "get help"
|
||||
REPORT_A_BUG_KEY: Final = "report a bug"
|
||||
ABOUT_KEY: Final = "about"
|
||||
|
||||
PageIcon: TypeAlias = Union[AtomicImage, str]
|
||||
Layout: TypeAlias = Literal["centered", "wide"]
|
||||
InitialSideBarState: TypeAlias = Literal["auto", "expanded", "collapsed"]
|
||||
_GetHelp: TypeAlias = Literal["Get help", "Get Help", "get help"]
|
||||
_ReportABug: TypeAlias = Literal["Report a bug", "report a bug"]
|
||||
_About: TypeAlias = Literal["About", "about"]
|
||||
MenuKey: TypeAlias = Literal[_GetHelp, _ReportABug, _About]
|
||||
MenuItems: TypeAlias = Mapping[MenuKey, Union[str, None]]
|
||||
|
||||
# Emojis recommended by https://share.streamlit.io/rensdimmendaal/emoji-recommender/main/app/streamlit.py
|
||||
# for the term "streamlit". Watch out for zero-width joiners,
|
||||
# as they won't parse correctly in the list() call!
|
||||
RANDOM_EMOJIS: Final = list(
|
||||
"🔥™🎉🚀🌌💣✨🌙🎆🎇💥🤩🤙🌛🤘⬆💡🤪🥂⚡💨🌠🎊🍿😛🔮🤟🌃🍃🍾💫▪🌴🎈🎬🌀🎄😝☔⛽🍂💃😎🍸🎨🥳☀😍🅱🌞😻🌟😜💦💅🦄😋😉👻🍁🤤👯🌻‼🌈👌🎃💛😚🔫🙌👽🍬🌅☁🍷👭☕🌚💁👅🥰🍜😌🎥🕺❕🧡☄💕🍻✅🌸🚬🤓🍹®☺💪😙☘🤠✊🤗🍵🤞😂💯😏📻🎂💗💜🌊❣🌝😘💆🤑🌿🦋😈⛄🚿😊🌹🥴😽💋😭🖤🙆👐⚪💟☃🙈🍭💻🥀🚗🤧🍝💎💓🤝💄💖🔞⁉⏰🕊🎧☠♥🌳🏾🙉⭐💊🍳🌎🙊💸❤🔪😆🌾✈📚💀🏠✌🏃🌵🚨💂🤫🤭😗😄🍒👏🙃🖖💞😅🎅🍄🆓👉💩🔊🤷⌚👸😇🚮💏👳🏽💘💿💉👠🎼🎶🎤👗❄🔐🎵🤒🍰👓🏄🌲🎮🙂📈🚙📍😵🗣❗🌺🙄👄🚘🥺🌍🏡♦💍🌱👑👙☑👾🍩🥶📣🏼🤣☯👵🍫➡🎀😃✋🍞🙇😹🙏👼🐝⚫🎁🍪🔨🌼👆👀😳🌏📖👃🎸👧💇🔒💙😞⛅🏻🍴😼🗿🍗♠🦁✔🤖☮🐢🐎💤😀🍺😁😴📺☹😲👍🎭💚🍆🍋🔵🏁🔴🔔🧐👰☎🏆🤡🐠📲🙋📌🐬✍🔑📱💰🐱💧🎓🍕👟🐣👫🍑😸🍦👁🆗🎯📢🚶🦅🐧💢🏀🚫💑🐟🌽🏊🍟💝💲🐍🍥🐸☝♣👊⚓❌🐯🏈📰🌧👿🐳💷🐺📞🆒🍀🤐🚲🍔👹🙍🌷🙎🐥💵🔝📸⚠❓🎩✂🍼😑⬇⚾🍎💔🐔⚽💭🏌🐷🍍✖🍇📝🍊🐙👋🤔🥊🗽🐑🐘🐰💐🐴♀🐦🍓✏👂🏴👇🆘😡🏉👩💌😺✝🐼🐒🐶👺🖕👬🍉🐻🐾⬅⏬▶👮🍌♂🔸👶🐮👪⛳🐐🎾🐕👴🐨🐊🔹©🎣👦👣👨👈💬⭕📹📷"
|
||||
)
|
||||
|
||||
# Also pick out some vanity emojis.
|
||||
ENG_EMOJIS: Final = [
|
||||
"🎈", # st.balloons 🎈🎈
|
||||
"🤓", # Abhi
|
||||
"🏈", # Amey
|
||||
"🚲", # Thiago
|
||||
"🐧", # Matteo
|
||||
"🦒", # Ken
|
||||
"🐳", # Karrie
|
||||
"🕹️", # Jonathan
|
||||
"🇦🇲", # Henrikh
|
||||
"🎸", # Guido
|
||||
"🦈", # Austin
|
||||
"💎", # Emiliano
|
||||
"👩🎤", # Naomi
|
||||
"🧙♂️", # Jon
|
||||
"🐻", # Brandon
|
||||
"🎎", # James
|
||||
# TODO: Solicit emojis from the rest of Streamlit
|
||||
]
|
||||
|
||||
|
||||
def _lower_clean_dict_keys(dict: MenuItems) -> dict[str, Any]:
|
||||
return {str(k).lower().strip(): v for k, v in dict.items()}
|
||||
|
||||
|
||||
def _get_favicon_string(page_icon: PageIcon) -> str:
|
||||
"""Return the string to pass to the frontend to have it show
|
||||
the given PageIcon.
|
||||
|
||||
If page_icon is a string that looks like an emoji (or an emoji shortcode),
|
||||
we return it as-is. Otherwise we use `image_to_url` to return a URL.
|
||||
|
||||
(If `image_to_url` raises an error and page_icon is a string, return
|
||||
the unmodified page_icon string instead of re-raising the error.)
|
||||
"""
|
||||
|
||||
# Choose a random emoji.
|
||||
if page_icon == "random":
|
||||
return get_random_emoji()
|
||||
|
||||
# If page_icon is an emoji, return it as is.
|
||||
if isinstance(page_icon, str) and is_emoji(page_icon):
|
||||
return f"emoji:{page_icon}"
|
||||
|
||||
if isinstance(page_icon, str) and page_icon.startswith(":material"):
|
||||
return validate_material_icon(page_icon)
|
||||
|
||||
# Convert Path to string if necessary
|
||||
if isinstance(page_icon, Path):
|
||||
page_icon = str(page_icon)
|
||||
|
||||
# Fall back to image_to_url.
|
||||
try:
|
||||
return image_to_url(
|
||||
page_icon,
|
||||
width=-1, # Always use full width for favicons
|
||||
clamp=False,
|
||||
channels="RGB",
|
||||
output_format="auto",
|
||||
image_id="favicon",
|
||||
)
|
||||
except Exception:
|
||||
if isinstance(page_icon, str):
|
||||
# This fall-thru handles emoji shortcode strings (e.g. ":shark:"),
|
||||
# which aren't valid filenames and so will cause an Exception from
|
||||
# `image_to_url`.
|
||||
return page_icon
|
||||
raise
|
||||
|
||||
|
||||
@gather_metrics("set_page_config")
|
||||
def set_page_config(
|
||||
page_title: str | None = None,
|
||||
page_icon: PageIcon | None = None,
|
||||
layout: Layout = "centered",
|
||||
initial_sidebar_state: InitialSideBarState = "auto",
|
||||
menu_items: MenuItems | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Configures the default settings of the page.
|
||||
|
||||
.. note::
|
||||
This must be the first Streamlit command used on an app page, and must only
|
||||
be set once per page.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
page_title: str or None
|
||||
The page title, shown in the browser tab. If None, defaults to the
|
||||
filename of the script ("app.py" would show "app • Streamlit").
|
||||
|
||||
page_icon : Anything supported by st.image (except list), str, or None
|
||||
The page favicon. If ``page_icon`` is ``None`` (default), the favicon
|
||||
will be a monochrome Streamlit logo.
|
||||
|
||||
In addition to the types supported by |st.image|_ (except list), the
|
||||
following strings are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``page_icon="🦈"``.
|
||||
|
||||
- An emoji short code. For example, you can set ``page_icon=":shark:"``.
|
||||
For a list of all supported codes, see
|
||||
https://share.streamlit.io/streamlit/emoji-shortcodes.
|
||||
|
||||
- The string literal, ``"random"``. You can set ``page_icon="random"``
|
||||
to set a random emoji from the supported list above.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``page_icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
.. note::
|
||||
Colors are not supported for Material icons. When you use a
|
||||
Material icon for favicon, it will be black, regardless of browser
|
||||
theme.
|
||||
|
||||
.. |st.image| replace:: ``st.image``
|
||||
.. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image
|
||||
|
||||
layout: "centered" or "wide"
|
||||
How the page content should be laid out. Defaults to "centered",
|
||||
which constrains the elements into a centered column of fixed width;
|
||||
"wide" uses the entire screen.
|
||||
|
||||
initial_sidebar_state: "auto", "expanded", or "collapsed"
|
||||
How the sidebar should start out. Defaults to "auto",
|
||||
which hides the sidebar on small devices and shows it otherwise.
|
||||
"expanded" shows the sidebar initially; "collapsed" hides it.
|
||||
In most cases, you should just use "auto", otherwise the app will
|
||||
look bad when embedded and viewed on mobile.
|
||||
|
||||
menu_items: dict
|
||||
Configure the menu that appears on the top-right side of this app.
|
||||
The keys in this dict denote the menu item you'd like to configure:
|
||||
|
||||
- "Get help": str or None
|
||||
The URL this menu item should point to.
|
||||
If None, hides this menu item.
|
||||
- "Report a Bug": str or None
|
||||
The URL this menu item should point to.
|
||||
If None, hides this menu item.
|
||||
- "About": str or None
|
||||
A markdown string to show in the About dialog.
|
||||
If None, only shows Streamlit's default About text.
|
||||
|
||||
The URL may also refer to an email address e.g. ``mailto:john@example.com``.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.set_page_config(
|
||||
... page_title="Ex-stream-ly Cool App",
|
||||
... page_icon="🧊",
|
||||
... layout="wide",
|
||||
... initial_sidebar_state="expanded",
|
||||
... menu_items={
|
||||
... 'Get Help': 'https://www.extremelycoolapp.com/help',
|
||||
... 'Report a bug': "https://www.extremelycoolapp.com/bug",
|
||||
... 'About': "# This is a header. This is an *extremely* cool app!"
|
||||
... }
|
||||
... )
|
||||
"""
|
||||
|
||||
msg = ForwardProto()
|
||||
|
||||
if page_title is not None:
|
||||
msg.page_config_changed.title = page_title
|
||||
|
||||
if page_icon is not None:
|
||||
msg.page_config_changed.favicon = _get_favicon_string(page_icon)
|
||||
|
||||
pb_layout: PageConfigProto.Layout.ValueType
|
||||
if layout == "centered":
|
||||
pb_layout = PageConfigProto.CENTERED
|
||||
elif layout == "wide":
|
||||
pb_layout = PageConfigProto.WIDE
|
||||
else:
|
||||
raise StreamlitInvalidPageLayoutError(layout=layout)
|
||||
|
||||
msg.page_config_changed.layout = pb_layout
|
||||
|
||||
pb_sidebar_state: PageConfigProto.SidebarState.ValueType
|
||||
if initial_sidebar_state == "auto":
|
||||
pb_sidebar_state = PageConfigProto.AUTO
|
||||
elif initial_sidebar_state == "expanded":
|
||||
pb_sidebar_state = PageConfigProto.EXPANDED
|
||||
elif initial_sidebar_state == "collapsed":
|
||||
pb_sidebar_state = PageConfigProto.COLLAPSED
|
||||
else:
|
||||
raise StreamlitInvalidSidebarStateError(
|
||||
initial_sidebar_state=initial_sidebar_state
|
||||
)
|
||||
|
||||
msg.page_config_changed.initial_sidebar_state = pb_sidebar_state
|
||||
|
||||
if menu_items is not None:
|
||||
lowercase_menu_items = cast("MenuItems", _lower_clean_dict_keys(menu_items))
|
||||
validate_menu_items(lowercase_menu_items)
|
||||
menu_items_proto = msg.page_config_changed.menu_items
|
||||
set_menu_items_proto(lowercase_menu_items, menu_items_proto)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is None:
|
||||
return
|
||||
ctx.enqueue(msg)
|
||||
|
||||
|
||||
def get_random_emoji() -> str:
|
||||
# Weigh our emojis 10x, cuz we're awesome!
|
||||
# TODO: fix the random seed with a hash of the user's app code, for stability?
|
||||
return random.choice(RANDOM_EMOJIS + 10 * ENG_EMOJIS)
|
||||
|
||||
|
||||
def set_menu_items_proto(lowercase_menu_items, menu_items_proto) -> None:
|
||||
if GET_HELP_KEY in lowercase_menu_items:
|
||||
if lowercase_menu_items[GET_HELP_KEY] is not None:
|
||||
menu_items_proto.get_help_url = lowercase_menu_items[GET_HELP_KEY]
|
||||
else:
|
||||
menu_items_proto.hide_get_help = True
|
||||
|
||||
if REPORT_A_BUG_KEY in lowercase_menu_items:
|
||||
if lowercase_menu_items[REPORT_A_BUG_KEY] is not None:
|
||||
menu_items_proto.report_a_bug_url = lowercase_menu_items[REPORT_A_BUG_KEY]
|
||||
else:
|
||||
menu_items_proto.hide_report_a_bug = True
|
||||
|
||||
if ABOUT_KEY in lowercase_menu_items:
|
||||
if lowercase_menu_items[ABOUT_KEY] is not None:
|
||||
menu_items_proto.about_section_md = dedent(lowercase_menu_items[ABOUT_KEY])
|
||||
|
||||
|
||||
def validate_menu_items(menu_items: MenuItems) -> None:
|
||||
for k, v in menu_items.items():
|
||||
if not valid_menu_item_key(k):
|
||||
raise StreamlitInvalidMenuItemKeyError(key=k)
|
||||
if v is not None and (
|
||||
not is_url(v, ("http", "https", "mailto")) and k != ABOUT_KEY
|
||||
):
|
||||
raise StreamlitInvalidURLError(url=v)
|
||||
|
||||
|
||||
def valid_menu_item_key(key: str) -> TypeGuard[MenuKey]:
|
||||
return key in {GET_HELP_KEY, REPORT_A_BUG_KEY, ABOUT_KEY}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from streamlit import util
|
||||
from streamlit.components.types.base_component_registry import BaseComponentRegistry
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.logger import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.components.types.base_custom_component import BaseCustomComponent
|
||||
|
||||
_LOGGER: Final = get_logger(__name__)
|
||||
|
||||
|
||||
class LocalComponentRegistry(BaseComponentRegistry):
|
||||
def __init__(self) -> None:
|
||||
self._components: dict[str, BaseCustomComponent] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return util.repr_(self)
|
||||
|
||||
def register_component(self, component: BaseCustomComponent) -> None:
|
||||
"""Register a CustomComponent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
component : BaseCustomComponent
|
||||
The component to register.
|
||||
"""
|
||||
|
||||
# Validate the component's path
|
||||
abspath = component.abspath
|
||||
if abspath is not None and not os.path.isdir(abspath):
|
||||
raise StreamlitAPIException(f"No such component directory: '{abspath}'")
|
||||
|
||||
with self._lock:
|
||||
existing = self._components.get(component.name)
|
||||
self._components[component.name] = component
|
||||
|
||||
if existing is not None and component != existing:
|
||||
_LOGGER.warning(
|
||||
"%s overriding previously-registered %s",
|
||||
component,
|
||||
existing,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Registered component %s", component)
|
||||
|
||||
def get_component_path(self, name: str) -> str | None:
|
||||
"""Return the filesystem path for the component with the given name.
|
||||
|
||||
If no such component is registered, or if the component exists but is
|
||||
being served from a URL, return None instead.
|
||||
"""
|
||||
component = self._components.get(name, None)
|
||||
return component.abspath if component is not None else None
|
||||
|
||||
def get_module_name(self, name: str) -> str | None:
|
||||
component = self._components.get(name, None)
|
||||
return component.module_name if component is not None else None
|
||||
|
||||
def get_component(self, name: str) -> BaseCustomComponent | None:
|
||||
return self._components.get(name, None)
|
||||
|
||||
def get_components(self) -> list[BaseCustomComponent]:
|
||||
return list(self._components.values())
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.components.types.base_custom_component import BaseCustomComponent
|
||||
|
||||
|
||||
class BaseComponentRegistry(Protocol):
|
||||
"""Interface for ComponentRegistries."""
|
||||
|
||||
@abstractmethod
|
||||
def register_component(self, component: BaseCustomComponent) -> None:
|
||||
"""Register a CustomComponent.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
component : CustomComponent
|
||||
The component to register.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_component_path(self, name: str) -> str | None:
|
||||
"""Return the filesystem path for the component with the given name.
|
||||
|
||||
If no such component is registered, or if the component exists but is
|
||||
being served from a URL, return None instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: name of the component
|
||||
|
||||
Returns
|
||||
-------
|
||||
str or None
|
||||
The name of the specified component or None if no component with the given name has been registered.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_module_name(self, name: str) -> str | None:
|
||||
"""Return the module name for the component with the given name.
|
||||
|
||||
If no such component is registered, return None instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: name of the component
|
||||
|
||||
Returns
|
||||
-------
|
||||
str or None
|
||||
The module_name of the specified component or None if no component with the given name has been registered.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_component(self, name: str) -> BaseCustomComponent | None:
|
||||
"""Return the registered component with the given name.
|
||||
|
||||
If no such component is registered, return None instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: name of the component
|
||||
|
||||
Returns
|
||||
-------
|
||||
component or None
|
||||
The component with the provided name or None if component with the given name has been registered.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_components(self) -> list[BaseCustomComponent]:
|
||||
"""Returns a list of custom components that are registered in this registry.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[CustomComponents]
|
||||
A list of registered custom components.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from streamlit import util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.runtime.state.common import WidgetCallback
|
||||
|
||||
|
||||
class MarshallComponentException(StreamlitAPIException):
|
||||
"""Class for exceptions generated during custom component marshalling."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class BaseCustomComponent(ABC):
|
||||
"""Interface for CustomComponents."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
path: str | None = None,
|
||||
url: str | None = None,
|
||||
module_name: str | None = None,
|
||||
):
|
||||
if (path is None and url is None) or (path is not None and url is not None):
|
||||
raise StreamlitAPIException(
|
||||
"Either 'path' or 'url' must be set, but not both."
|
||||
)
|
||||
|
||||
self._name = name
|
||||
self._path = path
|
||||
self._url = url
|
||||
self._module_name = module_name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return util.repr_(self)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*args,
|
||||
default: Any = None,
|
||||
key: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
"""An alias for create_instance."""
|
||||
return self.create_instance(
|
||||
*args,
|
||||
default=default,
|
||||
key=key,
|
||||
on_change=on_change,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@property
|
||||
def abspath(self) -> str | None:
|
||||
if self._path is None:
|
||||
return None
|
||||
return os.path.abspath(self._path)
|
||||
|
||||
@property
|
||||
def module_name(self) -> str | None:
|
||||
return self._module_name
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def path(self) -> str | None:
|
||||
return self._path
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
return self._url
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"'{self.name}': {self.path if self.path is not None else self.url}"
|
||||
|
||||
@abstractmethod
|
||||
def __eq__(self, other) -> bool:
|
||||
"""Equality operator."""
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def __ne__(self, other) -> bool:
|
||||
"""Inequality operator."""
|
||||
return NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def create_instance(
|
||||
self,
|
||||
*args,
|
||||
default: Any = None,
|
||||
key: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
"""Create a new instance of the component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args
|
||||
Must be empty; all args must be named. (This parameter exists to
|
||||
enforce correct use of the function.)
|
||||
default: any or None
|
||||
The default return value for the component. This is returned when
|
||||
the component's frontend hasn't yet specified a value with
|
||||
`setComponentValue`.
|
||||
key: str or None
|
||||
If not None, this is the user key we use to generate the
|
||||
component's "widget ID".
|
||||
on_change: WidgetCallback or None
|
||||
An optional callback invoked when the widget's value changes. No arguments are passed to it.
|
||||
**kwargs
|
||||
Keyword args to pass to the component.
|
||||
|
||||
Raises
|
||||
------
|
||||
MarshallComponentException
|
||||
Raised when args is not empty or component cannot be marshalled.
|
||||
StreamlitAPIException
|
||||
Raised when PyArrow is not installed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
any or None
|
||||
The component's widget value.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Contains the files and modules for the exposed API."""
|
||||
|
||||
import streamlit
|
||||
from streamlit.components.v1.component_registry import declare_component
|
||||
|
||||
# `html` and `iframe` are part of Custom Components, so they appear in this
|
||||
# `streamlit.components.v1` namespace.
|
||||
html = streamlit._main._html
|
||||
iframe = streamlit._main._iframe
|
||||
|
||||
__all__ = [
|
||||
"declare_component",
|
||||
"html",
|
||||
"iframe",
|
||||
]
|
||||
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Data marshalling utilities for ArrowTable protobufs, which are used by
|
||||
CustomComponent for dataframe serialization.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from streamlit import dataframe_util
|
||||
from streamlit.elements.lib import pandas_styler_utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pandas import DataFrame, Index, Series
|
||||
|
||||
from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto
|
||||
|
||||
|
||||
def _maybe_tuple_to_list(item: Any) -> Any:
|
||||
"""Convert a tuple to a list. Leave as is if it's not a tuple."""
|
||||
return list(item) if isinstance(item, tuple) else item
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: ArrowTableProto, data: Any, default_uuid: str | None = None
|
||||
) -> None:
|
||||
"""Marshall data into an ArrowTable proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.ArrowTable
|
||||
Output. The protobuf for a Streamlit ArrowTable proto.
|
||||
|
||||
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict, or None
|
||||
Something that is or can be converted to a dataframe.
|
||||
|
||||
"""
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
pandas_styler_utils.marshall_styler(proto, data, default_uuid) # type: ignore
|
||||
|
||||
df = dataframe_util.convert_anything_to_pandas_df(data)
|
||||
_marshall_index(proto, df.index)
|
||||
_marshall_columns(proto, df.columns)
|
||||
_marshall_data(proto, df)
|
||||
|
||||
|
||||
def _marshall_index(proto: ArrowTableProto, index: Index) -> None:
|
||||
"""Marshall pandas.DataFrame index into an ArrowTable proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.ArrowTable
|
||||
Output. The protobuf for a Streamlit ArrowTable proto.
|
||||
|
||||
index : pd.Index
|
||||
Index to use for resulting frame.
|
||||
Will default to RangeIndex (0, 1, 2, ..., n) if no index is provided.
|
||||
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
index = map(_maybe_tuple_to_list, index.values)
|
||||
index_df = pd.DataFrame(index)
|
||||
proto.index = dataframe_util.convert_pandas_df_to_arrow_bytes(index_df)
|
||||
|
||||
|
||||
def _marshall_columns(proto: ArrowTableProto, columns: Series) -> None:
|
||||
"""Marshall pandas.DataFrame columns into an ArrowTable proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.ArrowTable
|
||||
Output. The protobuf for a Streamlit ArrowTable proto.
|
||||
|
||||
columns : Series
|
||||
Column labels to use for resulting frame.
|
||||
Will default to RangeIndex (0, 1, 2, ..., n) if no column labels are provided.
|
||||
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
columns = map(_maybe_tuple_to_list, columns.values)
|
||||
columns_df = pd.DataFrame(columns)
|
||||
proto.columns = dataframe_util.convert_pandas_df_to_arrow_bytes(columns_df)
|
||||
|
||||
|
||||
def _marshall_data(proto: ArrowTableProto, df: DataFrame) -> None:
|
||||
"""Marshall pandas.DataFrame data into an ArrowTable proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.ArrowTable
|
||||
Output. The protobuf for a Streamlit ArrowTable proto.
|
||||
|
||||
df : pandas.DataFrame
|
||||
A dataframe to marshall.
|
||||
|
||||
"""
|
||||
proto.data = dataframe_util.convert_pandas_df_to_arrow_bytes(df)
|
||||
|
||||
|
||||
def arrow_proto_to_dataframe(proto: ArrowTableProto) -> DataFrame:
|
||||
"""Convert ArrowTable proto to pandas.DataFrame.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.ArrowTable
|
||||
Output. pandas.DataFrame
|
||||
|
||||
"""
|
||||
|
||||
if dataframe_util.is_pyarrow_version_less_than("14.0.1"):
|
||||
raise RuntimeError(
|
||||
"The installed pyarrow version is not compatible with this component. "
|
||||
"Please upgrade to 14.0.1 or higher: pip install -U pyarrow"
|
||||
)
|
||||
|
||||
import pandas as pd
|
||||
|
||||
data = dataframe_util.convert_arrow_bytes_to_pandas_df(proto.data)
|
||||
index = dataframe_util.convert_arrow_bytes_to_pandas_df(proto.index)
|
||||
columns = dataframe_util.convert_arrow_bytes_to_pandas_df(proto.columns)
|
||||
|
||||
return pd.DataFrame(
|
||||
data.to_numpy(),
|
||||
index=index.to_numpy().T.tolist(),
|
||||
columns=columns.to_numpy().T.tolist(),
|
||||
)
|
||||
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from streamlit.components.v1.custom_component import CustomComponent
|
||||
from streamlit.runtime import get_instance
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import FrameType
|
||||
|
||||
from streamlit.components.types.base_component_registry import BaseComponentRegistry
|
||||
|
||||
|
||||
def _get_module_name(caller_frame: FrameType) -> str:
|
||||
# Get the caller's module name. `__name__` gives us the module's
|
||||
# fully-qualified name, which includes its package.
|
||||
module = inspect.getmodule(caller_frame)
|
||||
assert module is not None
|
||||
module_name = module.__name__
|
||||
|
||||
# If the caller was the main module that was executed (that is, if the
|
||||
# user executed `python my_component.py`), then this name will be
|
||||
# "__main__" instead of the actual package name. In this case, we use
|
||||
# the main module's filename, sans `.py` extension, as the component name.
|
||||
if module_name == "__main__":
|
||||
file_path = inspect.getfile(caller_frame)
|
||||
filename = os.path.basename(file_path)
|
||||
module_name, _ = os.path.splitext(filename)
|
||||
|
||||
return module_name
|
||||
|
||||
|
||||
def declare_component(
|
||||
name: str,
|
||||
path: str | Path | None = None,
|
||||
url: str | None = None,
|
||||
) -> CustomComponent:
|
||||
"""Create a custom component and register it if there is a ``ScriptRunContext``.
|
||||
|
||||
The component is not registered when there is no ``ScriptRunContext``.
|
||||
This can happen when a ``CustomComponent`` is executed as standalone
|
||||
command (e.g. for testing).
|
||||
|
||||
To use this function, import it from the ``streamlit.components.v1``
|
||||
module.
|
||||
|
||||
.. warning::
|
||||
Using ``st.components.v1.declare_component`` directly (instead of
|
||||
importing its module) is deprecated and will be disallowed in a later
|
||||
version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
A short, descriptive name for the component, like "slider".
|
||||
|
||||
path: str, Path, or None
|
||||
The path to serve the component's frontend files from. The path should
|
||||
be absolute. If ``path`` is ``None`` (default), Streamlit will serve
|
||||
the component from the location in ``url``. Either ``path`` or ``url``
|
||||
must be specified, but not both.
|
||||
|
||||
url: str or None
|
||||
The URL that the component is served from. If ``url`` is ``None``
|
||||
(default), Streamlit will serve the component from the location in
|
||||
``path``. Either ``path`` or ``url`` must be specified, but not both.
|
||||
|
||||
Returns
|
||||
-------
|
||||
CustomComponent
|
||||
A ``CustomComponent`` that can be called like a function.
|
||||
Calling the component will create a new instance of the component
|
||||
in the Streamlit app.
|
||||
|
||||
"""
|
||||
if path is not None and isinstance(path, Path):
|
||||
path = str(path)
|
||||
|
||||
# Get our stack frame.
|
||||
current_frame: FrameType | None = inspect.currentframe()
|
||||
assert current_frame is not None
|
||||
# Get the stack frame of our calling function.
|
||||
caller_frame = current_frame.f_back
|
||||
assert caller_frame is not None
|
||||
module_name = _get_module_name(caller_frame)
|
||||
|
||||
# Build the component name.
|
||||
component_name = f"{module_name}.{name}"
|
||||
|
||||
# Create our component object, and register it.
|
||||
component = CustomComponent(
|
||||
name=component_name, path=path, url=url, module_name=module_name
|
||||
)
|
||||
# the ctx can be None if a custom component script is run outside of Streamlit, e.g. via 'python ...'
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is not None:
|
||||
get_instance().component_registry.register_component(component)
|
||||
return component
|
||||
|
||||
|
||||
# Keep for backwards-compatibility for now as we don't know whether existing custom
|
||||
# components use this method. We made significant refactors to the custom component
|
||||
# registry code in https://github.com/streamlit/streamlit/pull/8193 and after
|
||||
# that is out in the wild, we can follow-up with more refactorings, e.g. remove
|
||||
# the following class and method. When we do that, we should conduct some testing with
|
||||
# popular custom components.
|
||||
class ComponentRegistry:
|
||||
@classmethod
|
||||
def instance(cls) -> BaseComponentRegistry:
|
||||
"""Returns the ComponentRegistry of the runtime instance."""
|
||||
|
||||
return get_instance().component_registry
|
||||
@@ -0,0 +1,38 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# The components.py file exists because existing custom components have started
|
||||
# to rely on internals of the components package. For example, streamlit-option-menu accesses
|
||||
# [register_widget](https://github.com/victoryhb/streamlit-option-menu/blob/master/streamlit_option_menu/streamlit_callback.py#L28),
|
||||
# which is only a transitive import through `streamlit.components.v1.custom_component`.
|
||||
# Since we do not know what other internals are used out in the wild, let's try to
|
||||
# model the old behavior and not to break things.
|
||||
|
||||
# This should be cleaned up in the future, e.g. as part of components v2.
|
||||
|
||||
from streamlit.components.v1.component_registry import (
|
||||
declare_component,
|
||||
)
|
||||
from streamlit.components.v1.custom_component import (
|
||||
CustomComponent,
|
||||
MarshallComponentException,
|
||||
)
|
||||
from streamlit.runtime.state import register_widget
|
||||
|
||||
__all__ = [
|
||||
"CustomComponent",
|
||||
"declare_component",
|
||||
"MarshallComponentException",
|
||||
"register_widget",
|
||||
]
|
||||
@@ -0,0 +1,243 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from streamlit.components.types.base_custom_component import BaseCustomComponent
|
||||
from streamlit.dataframe_util import is_dataframe_like
|
||||
from streamlit.delta_generator_singletons import get_dg_singleton_instance
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import check_cache_replay_rules
|
||||
from streamlit.elements.lib.utils import compute_and_register_element_id
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Components_pb2 import ArrowTable as ArrowTableProto
|
||||
from streamlit.proto.Components_pb2 import SpecialArg
|
||||
from streamlit.proto.Element_pb2 import Element
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.runtime.state import register_widget
|
||||
from streamlit.type_util import is_bytes_like, to_bytes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.runtime.state.common import WidgetCallback
|
||||
|
||||
|
||||
class MarshallComponentException(StreamlitAPIException):
|
||||
"""Class for exceptions generated during custom component marshalling."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CustomComponent(BaseCustomComponent):
|
||||
"""A Custom Component declaration."""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*args,
|
||||
default: Any = None,
|
||||
key: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
"""An alias for create_instance."""
|
||||
return self.create_instance(
|
||||
*args,
|
||||
default=default,
|
||||
key=key,
|
||||
on_change=on_change,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@gather_metrics("create_instance")
|
||||
def create_instance(
|
||||
self,
|
||||
*args,
|
||||
default: Any = None,
|
||||
key: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
"""Create a new instance of the component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args
|
||||
Must be empty; all args must be named. (This parameter exists to
|
||||
enforce correct use of the function.)
|
||||
default: any or None
|
||||
The default return value for the component. This is returned when
|
||||
the component's frontend hasn't yet specified a value with
|
||||
`setComponentValue`.
|
||||
key: str or None
|
||||
If not None, this is the user key we use to generate the
|
||||
component's "widget ID".
|
||||
on_change: WidgetCallback or None
|
||||
An optional callback invoked when the widget's value changes. No arguments are passed to it.
|
||||
**kwargs
|
||||
Keyword args to pass to the component.
|
||||
|
||||
Returns
|
||||
-------
|
||||
any or None
|
||||
The component's widget value.
|
||||
|
||||
"""
|
||||
if len(args) > 0:
|
||||
raise MarshallComponentException(f"Argument '{args[0]}' needs a label")
|
||||
|
||||
try:
|
||||
import pyarrow # noqa: F401, ICN001
|
||||
|
||||
from streamlit.components.v1 import component_arrow
|
||||
except ImportError:
|
||||
raise StreamlitAPIException(
|
||||
"""To use Custom Components in Streamlit, you need to install
|
||||
PyArrow. To do so locally:
|
||||
|
||||
`pip install pyarrow`
|
||||
|
||||
And if you're using Streamlit Cloud, add "pyarrow" to your requirements.txt."""
|
||||
)
|
||||
|
||||
check_cache_replay_rules()
|
||||
# In addition to the custom kwargs passed to the component, we also
|
||||
# send the special 'default' and 'key' params to the component
|
||||
# frontend.
|
||||
all_args = dict(kwargs, default=default, key=key)
|
||||
|
||||
json_args = {}
|
||||
special_args = []
|
||||
for arg_name, arg_val in all_args.items():
|
||||
if is_bytes_like(arg_val):
|
||||
bytes_arg = SpecialArg()
|
||||
bytes_arg.key = arg_name
|
||||
bytes_arg.bytes = to_bytes(arg_val)
|
||||
special_args.append(bytes_arg)
|
||||
elif is_dataframe_like(arg_val):
|
||||
dataframe_arg = SpecialArg()
|
||||
dataframe_arg.key = arg_name
|
||||
component_arrow.marshall(dataframe_arg.arrow_dataframe.data, arg_val)
|
||||
special_args.append(dataframe_arg)
|
||||
else:
|
||||
json_args[arg_name] = arg_val
|
||||
|
||||
try:
|
||||
serialized_json_args = json.dumps(json_args)
|
||||
except Exception as ex:
|
||||
raise MarshallComponentException(
|
||||
"Could not convert component args to JSON", ex
|
||||
)
|
||||
|
||||
def marshall_component(dg: DeltaGenerator, element: Element) -> Any:
|
||||
element.component_instance.component_name = self.name
|
||||
element.component_instance.form_id = current_form_id(dg)
|
||||
if self.url is not None:
|
||||
element.component_instance.url = self.url
|
||||
|
||||
# Normally, a widget's element_hash (which determines
|
||||
# its identity across multiple runs of an app) is computed
|
||||
# by hashing its arguments. This means that, if any of the arguments
|
||||
# to the widget are changed, Streamlit considers it a new widget
|
||||
# instance and it loses its previous state.
|
||||
#
|
||||
# However! If a *component* has a `key` argument, then the
|
||||
# component's hash identity is determined by entirely by
|
||||
# `component_name + url + key`. This means that, when `key`
|
||||
# exists, the component will maintain its identity even when its
|
||||
# other arguments change, and the component's iframe won't be
|
||||
# remounted on the frontend.
|
||||
|
||||
def marshall_element_args():
|
||||
element.component_instance.json_args = serialized_json_args
|
||||
element.component_instance.special_args.extend(special_args)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if key is None:
|
||||
marshall_element_args()
|
||||
computed_id = compute_and_register_element_id(
|
||||
"component_instance",
|
||||
user_key=key,
|
||||
form_id=current_form_id(dg),
|
||||
name=self.name,
|
||||
url=self.url,
|
||||
json_args=serialized_json_args,
|
||||
special_args=special_args,
|
||||
)
|
||||
else:
|
||||
computed_id = compute_and_register_element_id(
|
||||
"component_instance",
|
||||
user_key=key,
|
||||
form_id=current_form_id(dg),
|
||||
name=self.name,
|
||||
url=self.url,
|
||||
)
|
||||
element.component_instance.id = computed_id
|
||||
|
||||
def deserialize_component(ui_value, widget_id=""):
|
||||
# ui_value is an object from json, an ArrowTable proto, or a bytearray
|
||||
return ui_value
|
||||
|
||||
component_state = register_widget(
|
||||
element.component_instance.id,
|
||||
deserializer=deserialize_component,
|
||||
serializer=lambda x: x,
|
||||
ctx=ctx,
|
||||
on_change_handler=on_change,
|
||||
value_type="json_value",
|
||||
)
|
||||
widget_value = component_state.value
|
||||
|
||||
if key is not None:
|
||||
marshall_element_args()
|
||||
|
||||
if widget_value is None:
|
||||
widget_value = default
|
||||
elif isinstance(widget_value, ArrowTableProto):
|
||||
widget_value = component_arrow.arrow_proto_to_dataframe(widget_value)
|
||||
return widget_value
|
||||
|
||||
# We currently only support writing to st._main, but this will change
|
||||
# when we settle on an improved API in a post-layout world.
|
||||
dg = get_dg_singleton_instance().main_dg
|
||||
|
||||
element = Element()
|
||||
return_value = marshall_component(dg, element)
|
||||
|
||||
dg._enqueue("component_instance", element.component_instance)
|
||||
return return_value
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
"""Equality operator."""
|
||||
return (
|
||||
isinstance(other, CustomComponent)
|
||||
and self.name == other.name
|
||||
and self.path == other.path
|
||||
and self.url == other.url
|
||||
and self.module_name == other.module_name
|
||||
)
|
||||
|
||||
def __ne__(self, other) -> bool:
|
||||
"""Inequality operator."""
|
||||
|
||||
# we have to use "not X == Y"" here because if we use "X != Y"
|
||||
# we call __ne__ again and end up in recursion
|
||||
return not self == other
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"'{self.name}': {self.path if self.path is not None else self.url}"
|
||||
1664
myenv/lib/python3.11/site-packages/streamlit/config.py
Normal file
1664
myenv/lib/python3.11/site-packages/streamlit/config.py
Normal file
File diff suppressed because it is too large
Load Diff
312
myenv/lib/python3.11/site-packages/streamlit/config_option.py
Normal file
312
myenv/lib/python3.11/site-packages/streamlit/config_option.py
Normal file
@@ -0,0 +1,312 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Class to store a key-value pair for the config system."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import re
|
||||
import textwrap
|
||||
from typing import Any, Callable
|
||||
|
||||
from streamlit.string_util import to_snake_case
|
||||
from streamlit.util import repr_
|
||||
|
||||
|
||||
class ConfigOption:
|
||||
'''Stores a Streamlit configuration option.
|
||||
|
||||
A configuration option, like 'browser.serverPort', which indicates which port
|
||||
to use when connecting to the proxy. There are two ways to create a
|
||||
ConfigOption:
|
||||
|
||||
Simple ConfigOptions are created as follows:
|
||||
|
||||
ConfigOption('browser.serverPort',
|
||||
description = 'Connect to the proxy at this port.',
|
||||
default_val = 8501)
|
||||
|
||||
More complex config options resolve their values at runtime as follows:
|
||||
|
||||
@ConfigOption('browser.serverPort')
|
||||
def _proxy_port():
|
||||
"""Connect to the proxy at this port.
|
||||
|
||||
Defaults to 8501.
|
||||
"""
|
||||
return 8501
|
||||
|
||||
NOTE: For complex config options, the function is called each time the
|
||||
option.value is evaluated!
|
||||
|
||||
Attributes
|
||||
----------
|
||||
key : str
|
||||
The fully qualified section.name
|
||||
value : any
|
||||
The value for this option. If this is a complex config option then
|
||||
the callback is called EACH TIME value is evaluated.
|
||||
section : str
|
||||
The section of this option. Example: 'global'.
|
||||
name : str
|
||||
See __init__.
|
||||
description : str
|
||||
See __init__.
|
||||
where_defined : str
|
||||
Indicates which file set this config option.
|
||||
ConfigOption.DEFAULT_DEFINITION means this file.
|
||||
is_default: bool
|
||||
True if the config value is equal to its default value.
|
||||
visibility : {"visible", "hidden"}
|
||||
See __init__.
|
||||
scriptable : bool
|
||||
See __init__.
|
||||
deprecated: bool
|
||||
See __init__.
|
||||
deprecation_text : str or None
|
||||
See __init__.
|
||||
expiration_date : str or None
|
||||
See __init__.
|
||||
replaced_by : str or None
|
||||
See __init__.
|
||||
sensitive : bool
|
||||
See __init__.
|
||||
env_var: str
|
||||
The name of the environment variable that can be used to set the option.
|
||||
'''
|
||||
|
||||
# This is a special value for ConfigOption.where_defined which indicates
|
||||
# that the option default was not overridden.
|
||||
DEFAULT_DEFINITION = "<default>"
|
||||
|
||||
# This is a special value for ConfigOption.where_defined which indicates
|
||||
# that the options was defined by Streamlit's own code.
|
||||
STREAMLIT_DEFINITION = "<streamlit>"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
key: str,
|
||||
description: str | None = None,
|
||||
default_val: Any | None = None,
|
||||
visibility: str = "visible",
|
||||
scriptable: bool = False,
|
||||
deprecated: bool = False,
|
||||
deprecation_text: str | None = None,
|
||||
expiration_date: str | None = None,
|
||||
replaced_by: str | None = None,
|
||||
type_: type = str,
|
||||
sensitive: bool = False,
|
||||
):
|
||||
"""Create a ConfigOption with the given name.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : str
|
||||
Should be of the form "section.optionName"
|
||||
Examples: server.name, deprecation.v1_0_featureName
|
||||
description : str
|
||||
Like a comment for the config option.
|
||||
default_val : any
|
||||
The value for this config option.
|
||||
visibility : {"visible", "hidden"}
|
||||
Whether this option should be shown to users.
|
||||
scriptable : bool
|
||||
Whether this config option can be set within a user script.
|
||||
deprecated: bool
|
||||
Whether this config option is deprecated.
|
||||
deprecation_text : str or None
|
||||
Required if deprecated == True. Set this to a string explaining
|
||||
what to use instead.
|
||||
expiration_date : str or None
|
||||
Required if deprecated == True. set this to the date at which it
|
||||
will no longer be accepted. Format: 'YYYY-MM-DD'.
|
||||
replaced_by : str or None
|
||||
If this is option has been deprecated in favor or another option,
|
||||
set this to the path to the new option. Example:
|
||||
'server.runOnSave'. If this is set, the 'deprecated' option
|
||||
will automatically be set to True, and deprecation_text will have a
|
||||
meaningful default (unless you override it).
|
||||
type_ : one of str, int, float or bool
|
||||
Useful to cast the config params sent by cmd option parameter.
|
||||
sensitive: bool
|
||||
Sensitive configuration options cannot be set by CLI parameter.
|
||||
"""
|
||||
# Parse out the section and name.
|
||||
self.key = key
|
||||
key_format = (
|
||||
# Capture a group called "section"
|
||||
r"(?P<section>"
|
||||
# Matching text comprised of letters and numbers that begins
|
||||
# with a lowercase letter with an optional "_" preceding it.
|
||||
# Examples: "_section", "section1"
|
||||
r"\_?[a-z][a-zA-Z0-9]*"
|
||||
# Handling zero or additional parts, separated by period
|
||||
# Examples: "_section.subsection", "section1._section2"
|
||||
r"(\.[a-z][a-zA-Z0-9]*)*"
|
||||
r")"
|
||||
# The final period, separating section and name
|
||||
r"\."
|
||||
# Capture a group called "name"
|
||||
r"(?P<name>"
|
||||
# Match text comprised of letters and numbers beginning with a
|
||||
# lowercase letter.
|
||||
# Examples: "name", "nameOfConfig", "config1"
|
||||
r"[a-z][a-zA-Z0-9]*"
|
||||
r")$"
|
||||
)
|
||||
match = re.match(key_format, self.key)
|
||||
assert match, f'Key "{self.key}" has invalid format.'
|
||||
self.section, self.name = match.group("section"), match.group("name")
|
||||
|
||||
self.description = description
|
||||
|
||||
self.visibility = visibility
|
||||
self.scriptable = scriptable
|
||||
self.default_val = default_val
|
||||
self.deprecated = deprecated
|
||||
self.replaced_by = replaced_by
|
||||
self.is_default = True
|
||||
self._get_val_func: Callable[[], Any] | None = None
|
||||
self.where_defined = ConfigOption.DEFAULT_DEFINITION
|
||||
self.type = type_
|
||||
self.sensitive = sensitive
|
||||
# infer multiple values if the default value is a list or tuple
|
||||
self.multiple = isinstance(default_val, (list, tuple))
|
||||
|
||||
if self.replaced_by:
|
||||
self.deprecated = True
|
||||
if deprecation_text is None:
|
||||
deprecation_text = "Replaced by %s." % self.replaced_by
|
||||
|
||||
if self.deprecated:
|
||||
assert expiration_date, "expiration_date is required for deprecated items"
|
||||
assert deprecation_text, "deprecation_text is required for deprecated items"
|
||||
self.expiration_date = expiration_date
|
||||
self.deprecation_text = textwrap.dedent(deprecation_text)
|
||||
|
||||
self.set_value(default_val)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return repr_(self)
|
||||
|
||||
def __call__(self, get_val_func: Callable[[], Any]) -> ConfigOption:
|
||||
"""Assign a function to compute the value for this option.
|
||||
|
||||
This method is called when ConfigOption is used as a decorator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
get_val_func : function
|
||||
A function which will be called to get the value of this parameter.
|
||||
We will use its docString as the description.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ConfigOption
|
||||
Returns self, which makes testing easier. See config_test.py.
|
||||
|
||||
"""
|
||||
assert get_val_func.__doc__, (
|
||||
"Complex config options require doc strings for their description."
|
||||
)
|
||||
self.description = get_val_func.__doc__
|
||||
self._get_val_func = get_val_func
|
||||
return self
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
"""Get the value of this config option."""
|
||||
if self._get_val_func is None:
|
||||
return None
|
||||
return self._get_val_func()
|
||||
|
||||
def set_value(self, value: Any, where_defined: str | None = None) -> None:
|
||||
"""Set the value of this option.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value
|
||||
The new value for this parameter.
|
||||
where_defined : str
|
||||
New value to remember where this parameter was set.
|
||||
|
||||
"""
|
||||
self._get_val_func = lambda: value
|
||||
|
||||
if where_defined is None:
|
||||
self.where_defined = ConfigOption.DEFAULT_DEFINITION
|
||||
else:
|
||||
self.where_defined = where_defined
|
||||
|
||||
self.is_default = value == self.default_val
|
||||
|
||||
if self.deprecated and self.where_defined != ConfigOption.DEFAULT_DEFINITION:
|
||||
if self.is_expired():
|
||||
# Import here to avoid circular imports
|
||||
from streamlit.logger import get_logger
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER.error(
|
||||
textwrap.dedent(
|
||||
f"""
|
||||
════════════════════════════════════════════════
|
||||
{self.key} IS NO LONGER SUPPORTED.
|
||||
|
||||
{self.deprecation_text}
|
||||
|
||||
Please update {self.where_defined}.
|
||||
════════════════════════════════════════════════
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Import here to avoid circular imports
|
||||
from streamlit.logger import get_logger
|
||||
|
||||
LOGGER = get_logger(__name__)
|
||||
LOGGER.warning(
|
||||
textwrap.dedent(
|
||||
f"""s
|
||||
════════════════════════════════════════════════
|
||||
{self.key} IS DEPRECATED.
|
||||
{self.deprecation_text}
|
||||
|
||||
This option will be removed on or after {self.expiration_date}.
|
||||
|
||||
Please update {self.where_defined}.
|
||||
════════════════════════════════════════════════
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Returns true if expiration_date is in the past."""
|
||||
if not self.deprecated:
|
||||
return False
|
||||
|
||||
expiration_date = _parse_yyyymmdd_str(self.expiration_date)
|
||||
now = datetime.datetime.now()
|
||||
return now > expiration_date
|
||||
|
||||
@property
|
||||
def env_var(self):
|
||||
"""Get the name of the environment variable that can be used to set the option."""
|
||||
name = self.key.replace(".", "_")
|
||||
return f"STREAMLIT_{to_snake_case(name).upper()}"
|
||||
|
||||
|
||||
def _parse_yyyymmdd_str(date_str: str) -> datetime.datetime:
|
||||
year, month, day = (int(token) for token in date_str.split("-", 2))
|
||||
return datetime.datetime(year, month, day)
|
||||
177
myenv/lib/python3.11/site-packages/streamlit/config_util.py
Normal file
177
myenv/lib/python3.11/site-packages/streamlit/config_util.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from streamlit import cli_util
|
||||
from streamlit.config_option import ConfigOption
|
||||
|
||||
|
||||
def server_option_changed(
|
||||
old_options: dict[str, ConfigOption], new_options: dict[str, ConfigOption]
|
||||
) -> bool:
|
||||
"""Return True if and only if an option in the server section differs
|
||||
between old_options and new_options.
|
||||
"""
|
||||
for opt_name, opt_val in old_options.items():
|
||||
if not opt_name.startswith("server"):
|
||||
continue
|
||||
|
||||
old_val = opt_val.value
|
||||
new_val = new_options[opt_name].value
|
||||
if old_val != new_val:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def show_config(
|
||||
section_descriptions: dict[str, str],
|
||||
config_options: dict[str, ConfigOption],
|
||||
) -> None:
|
||||
"""Print the given config sections/options to the terminal."""
|
||||
|
||||
out = []
|
||||
out.append(
|
||||
_clean(
|
||||
"""
|
||||
# Below are all the sections and options you can have in
|
||||
~/.streamlit/config.toml.
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
def append_desc(text):
|
||||
out.append("# " + cli_util.style_for_cli(text, bold=True))
|
||||
|
||||
def append_comment(text):
|
||||
out.append("# " + cli_util.style_for_cli(text))
|
||||
|
||||
def append_section(text):
|
||||
out.append(cli_util.style_for_cli(text, bold=True, fg="green"))
|
||||
|
||||
def append_setting(text):
|
||||
out.append(cli_util.style_for_cli(text, fg="green"))
|
||||
|
||||
for section, _ in section_descriptions.items():
|
||||
# We inject a fake config section used for unit tests that we exclude here as
|
||||
# its options are often missing required properties, which confuses the code
|
||||
# below.
|
||||
if section == "_test":
|
||||
continue
|
||||
|
||||
section_options = {
|
||||
k: v
|
||||
for k, v in config_options.items()
|
||||
if v.section == section and v.visibility == "visible" and not v.is_expired()
|
||||
}
|
||||
|
||||
# Only show config header if section is non-empty.
|
||||
if len(section_options) == 0:
|
||||
continue
|
||||
|
||||
out.append("")
|
||||
append_section("[%s]" % section)
|
||||
out.append("")
|
||||
|
||||
for key, option in section_options.items():
|
||||
key = option.key.split(".")[-1]
|
||||
description_paragraphs = _clean_paragraphs(option.description or "")
|
||||
|
||||
last_paragraph_idx = len(description_paragraphs) - 1
|
||||
|
||||
for i, paragraph in enumerate(description_paragraphs):
|
||||
# Split paragraph into lines
|
||||
lines = paragraph.rstrip().split(
|
||||
"\n"
|
||||
) # Remove trailing newline characters
|
||||
|
||||
# If the first line is empty, remove it
|
||||
if lines and not lines[0].strip():
|
||||
lines = lines[1:]
|
||||
|
||||
# Choose function based on whether it's the first paragraph or not
|
||||
append_func = append_desc if i == 0 else append_comment
|
||||
|
||||
# Add comment character to each line and add to out
|
||||
for line in lines:
|
||||
append_func(line.lstrip())
|
||||
|
||||
# # Add a line break after a paragraph only if it's not the last paragraph
|
||||
if i != last_paragraph_idx:
|
||||
out.append("")
|
||||
|
||||
import toml
|
||||
|
||||
toml_default = toml.dumps({"default": option.default_val})
|
||||
toml_default = toml_default[10:].strip()
|
||||
|
||||
if len(toml_default) > 0:
|
||||
# Ensure a line break before appending "Default" comment, if not already there
|
||||
if out[-1] != "":
|
||||
out.append("")
|
||||
append_comment("Default: %s" % toml_default)
|
||||
else:
|
||||
# Don't say "Default: (unset)" here because this branch applies
|
||||
# to complex config settings too.
|
||||
pass
|
||||
|
||||
if option.deprecated:
|
||||
append_comment(cli_util.style_for_cli("DEPRECATED.", fg="yellow"))
|
||||
for line in _clean_paragraphs(option.deprecation_text):
|
||||
append_comment(line)
|
||||
append_comment(
|
||||
"This option will be removed on or after %s."
|
||||
% option.expiration_date
|
||||
)
|
||||
|
||||
option_is_manually_set = (
|
||||
option.where_defined != ConfigOption.DEFAULT_DEFINITION
|
||||
)
|
||||
|
||||
if option_is_manually_set:
|
||||
append_comment("The value below was set in %s" % option.where_defined)
|
||||
|
||||
toml_setting = toml.dumps({key: option.value})
|
||||
|
||||
if len(toml_setting) == 0:
|
||||
toml_setting = f"# {key} =\n"
|
||||
elif not option_is_manually_set:
|
||||
toml_setting = f"# {toml_setting}"
|
||||
|
||||
append_setting(toml_setting)
|
||||
|
||||
cli_util.print_to_cli("\n".join(out))
|
||||
|
||||
|
||||
def _clean(txt: str) -> str:
|
||||
"""Replace sequences of multiple spaces with a single space, excluding newlines.
|
||||
|
||||
Preserves leading and trailing spaces, and does not modify spaces in between lines.
|
||||
"""
|
||||
return re.sub(" +", " ", txt)
|
||||
|
||||
|
||||
def _clean_paragraphs(txt: str) -> list[str]:
|
||||
"""Split the text into paragraphs, preserve newlines within the paragraphs."""
|
||||
# Strip both leading and trailing newlines.
|
||||
txt = txt.strip("\n")
|
||||
paragraphs = txt.split("\n\n")
|
||||
cleaned_paragraphs = [
|
||||
"\n".join(_clean(line) for line in paragraph.split("\n"))
|
||||
for paragraph in paragraphs
|
||||
]
|
||||
return cleaned_paragraphs
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from streamlit.connections.base_connection import BaseConnection
|
||||
from streamlit.connections.snowflake_connection import SnowflakeConnection
|
||||
from streamlit.connections.snowpark_connection import SnowparkConnection
|
||||
from streamlit.connections.sql_connection import SQLConnection
|
||||
|
||||
ExperimentalBaseConnection = BaseConnection
|
||||
|
||||
__all__ = [
|
||||
"BaseConnection",
|
||||
"SnowflakeConnection",
|
||||
"SnowparkConnection",
|
||||
"SQLConnection",
|
||||
"ExperimentalBaseConnection",
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Generic, TypeVar
|
||||
|
||||
from streamlit.runtime.secrets import AttrDict, secrets_singleton
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
RawConnectionT = TypeVar("RawConnectionT")
|
||||
|
||||
|
||||
class BaseConnection(ABC, Generic[RawConnectionT]):
|
||||
"""The abstract base class that all Streamlit Connections must inherit from.
|
||||
|
||||
This base class provides connection authors with a standardized way to hook into the
|
||||
``st.connection()`` factory function: connection authors are required to provide an
|
||||
implementation for the abstract method ``_connect`` in their subclasses.
|
||||
|
||||
Additionally, it also provides a few methods/properties designed to make
|
||||
implementation of connections more convenient. See the docstrings for each of the
|
||||
methods of this class for more information
|
||||
|
||||
.. note::
|
||||
While providing an implementation of ``_connect`` is technically all that's
|
||||
required to define a valid connection, connections should also provide the user
|
||||
with context-specific ways of interacting with the underlying connection object.
|
||||
For example, the first-party SQLConnection provides a ``query()`` method for
|
||||
reads and a ``session`` property for more complex operations.
|
||||
"""
|
||||
|
||||
def __init__(self, connection_name: str, **kwargs) -> None:
|
||||
"""Create a BaseConnection.
|
||||
|
||||
This constructor is called by the connection factory machinery when a user
|
||||
script calls ``st.connection()``.
|
||||
|
||||
Subclasses of BaseConnection that want to overwrite this method should take care
|
||||
to also call the base class' implementation.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
connection_name : str
|
||||
The name of this connection. This corresponds to the
|
||||
``[connections.<connection_name>]`` config section in ``st.secrets``.
|
||||
kwargs : dict
|
||||
Any other kwargs to pass to this connection class' ``_connect`` method.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
"""
|
||||
self._connection_name = connection_name
|
||||
self._kwargs = kwargs
|
||||
|
||||
self._config_section_hash = calc_md5(json.dumps(self._secrets.to_dict()))
|
||||
secrets_singleton.file_change_listener.connect(self._on_secrets_changed)
|
||||
|
||||
self._raw_instance: RawConnectionT | None = self._connect(**kwargs)
|
||||
|
||||
def __del__(self) -> None:
|
||||
secrets_singleton.file_change_listener.disconnect(self._on_secrets_changed)
|
||||
|
||||
def __getattribute__(self, name: str) -> Any:
|
||||
try:
|
||||
return object.__getattribute__(self, name)
|
||||
except AttributeError as e:
|
||||
if hasattr(self._instance, name):
|
||||
raise AttributeError(
|
||||
f"`{name}` doesn't exist here, but you can call `._instance.{name}` instead"
|
||||
)
|
||||
raise e
|
||||
|
||||
# Methods with default implementations that we don't expect subclasses to want or
|
||||
# need to overwrite.
|
||||
def _on_secrets_changed(self, _) -> None:
|
||||
"""Reset the raw connection object when this connection's secrets change.
|
||||
|
||||
We don't expect either user scripts or connection authors to have to use or
|
||||
overwrite this method.
|
||||
"""
|
||||
new_hash = calc_md5(json.dumps(self._secrets.to_dict()))
|
||||
|
||||
# Only reset the connection if the secrets file section specific to this
|
||||
# connection has changed.
|
||||
if new_hash != self._config_section_hash:
|
||||
self._config_section_hash = new_hash
|
||||
self.reset()
|
||||
|
||||
@property
|
||||
def _secrets(self) -> AttrDict:
|
||||
"""Get the secrets for this connection from the corresponding st.secrets section.
|
||||
|
||||
We expect this property to be used primarily by connection authors when they
|
||||
are implementing their class' ``_connect`` method. User scripts should, for the
|
||||
most part, have no reason to use this property.
|
||||
"""
|
||||
connections_section = None
|
||||
if secrets_singleton.load_if_toml_exists():
|
||||
connections_section = secrets_singleton.get("connections")
|
||||
|
||||
if type(connections_section) is not AttrDict:
|
||||
return AttrDict({})
|
||||
|
||||
return connections_section.get(self._connection_name, AttrDict({}))
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset this connection so that it gets reinitialized the next time it's used.
|
||||
|
||||
This method can be useful when a connection has become stale, an auth token has
|
||||
expired, or in similar scenarios where a broken connection might be fixed by
|
||||
reinitializing it. Note that some connection methods may already use ``reset()``
|
||||
in their error handling code.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("my_conn")
|
||||
>>>
|
||||
>>> # Reset the connection before using it if it isn't healthy
|
||||
>>> # Note: is_healthy() isn't a real method and is just shown for example here.
|
||||
>>> if not conn.is_healthy():
|
||||
... conn.reset()
|
||||
>>>
|
||||
>>> # Do stuff with conn...
|
||||
"""
|
||||
self._raw_instance = None
|
||||
|
||||
@property
|
||||
def _instance(self) -> RawConnectionT:
|
||||
"""Get an instance of the underlying connection, creating a new one if needed."""
|
||||
if self._raw_instance is None:
|
||||
self._raw_instance = self._connect(**self._kwargs)
|
||||
|
||||
return self._raw_instance
|
||||
|
||||
# Abstract fields/methods that subclasses of BaseConnection must implement
|
||||
@abstractmethod
|
||||
def _connect(self, **kwargs) -> RawConnectionT:
|
||||
"""Create an instance of an underlying connection object.
|
||||
|
||||
This abstract method is the one method that we require subclasses of
|
||||
BaseConnection to provide an implementation for. It is called when first
|
||||
creating a connection and when reconnecting after a connection is reset.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kwargs : dict
|
||||
|
||||
Returns
|
||||
-------
|
||||
RawConnectionT
|
||||
The underlying connection object.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,561 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# NOTE: We won't always be able to import from snowflake.{connector, snowpark}.* so need
|
||||
# the `type: ignore` comment below, but that comment will explode if `warn-unused-ignores`
|
||||
# is turned on when the package is available. Unfortunately, mypy doesn't provide a good
|
||||
# way to configure this at a per-line level :(
|
||||
# mypy: no-warn-unused-ignores
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Final, cast
|
||||
|
||||
from streamlit import logger
|
||||
from streamlit.connections import BaseConnection
|
||||
from streamlit.connections.util import running_in_sis
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.caching import cache_data
|
||||
|
||||
_LOGGER: Final = logger.get_logger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import timedelta
|
||||
|
||||
from pandas import DataFrame
|
||||
from snowflake.connector.cursor import SnowflakeCursor # type:ignore[import]
|
||||
from snowflake.snowpark.session import Session # type:ignore[import]
|
||||
|
||||
from snowflake.connector import ( # type:ignore[import] # isort: skip
|
||||
SnowflakeConnection as InternalSnowflakeConnection,
|
||||
)
|
||||
|
||||
|
||||
class SnowflakeConnection(BaseConnection["InternalSnowflakeConnection"]):
|
||||
"""A connection to Snowflake using the Snowflake Connector for Python.
|
||||
|
||||
Initialize this connection object using ``st.connection("snowflake")`` or
|
||||
``st.connection("<name>", type="snowflake")``. Connection parameters for a
|
||||
SnowflakeConnection can be specified using ``secrets.toml`` and/or
|
||||
``**kwargs``. Connection parameters are passed to
|
||||
|snowflake.connector.connect()|.
|
||||
|
||||
When an app is running in Streamlit in Snowflake,
|
||||
``st.connection("snowflake")`` connects automatically using the app owner's
|
||||
role without further configuration. ``**kwargs`` will be ignored in this
|
||||
case. Use ``secrets.toml`` and ``**kwargs`` to configure your connection
|
||||
for local development.
|
||||
|
||||
SnowflakeConnection includes several convenience methods. For example, you
|
||||
can directly execute a SQL query with ``.query()`` or access the underlying
|
||||
Snowflake Connector object with ``.raw_connection``.
|
||||
|
||||
.. |snowflake.connector.connect()| replace:: ``snowflake.connector.connect()``
|
||||
.. _snowflake.connector.connect(): https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#label-snowflake-connector-methods-connect
|
||||
|
||||
.. Tip::
|
||||
`snowflake-snowpark-python <https://pypi.org/project/snowflake-snowpark-python/>`_
|
||||
must be installed in your environment to use this connection. You can
|
||||
install Snowflake extras along with Streamlit:
|
||||
|
||||
>>> pip install streamlit[snowflake]
|
||||
|
||||
.. Important::
|
||||
Account identifiers must be of the form ``<orgname>-<account_name>``
|
||||
where ``<orgname>`` is the name of your Snowflake organization and
|
||||
``<account_name>`` is the unique name of your account within your
|
||||
organization. This is dash-separated, not dot-separated like when used
|
||||
in SQL queries. For more information, see `Account identifiers
|
||||
<https://docs.snowflake.com/en/user-guide/admin-account-identifier>`_.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Configuration with Streamlit secrets**
|
||||
|
||||
You can configure your Snowflake connection using Streamlit's
|
||||
`Secrets management <https://docs.streamlit.io/develop/concepts/connections/secrets-management>`_.
|
||||
For example, if you have MFA enabled on your account, you can connect using
|
||||
`key-pair authentication <https://docs.snowflake.com/en/user-guide/key-pair-auth>`_.
|
||||
|
||||
``.streamlit/secrets.toml``:
|
||||
|
||||
>>> [connections.snowflake]
|
||||
>>> account = "xxx-xxx"
|
||||
>>> user = "xxx"
|
||||
>>> private_key_file = "/xxx/xxx/xxx.p8"
|
||||
>>> role = "xxx"
|
||||
>>> warehouse = "xxx"
|
||||
>>> database = "xxx"
|
||||
>>> schema = "xxx"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
|
||||
**Example 2: Configuration with keyword arguments and external authentication**
|
||||
|
||||
You can configure your Snowflake connection with keyword arguments. The
|
||||
keyword arguments are merged with (and take precedence over) the values in
|
||||
``secrets.toml``. However, if you name your connection ``"snowflake"`` and
|
||||
don't have a ``[connections.snowflake]`` dictionary in your
|
||||
``secrets.toml`` file, Streamlit will ignore any keyword arguments and use
|
||||
the default Snowflake connection as described in Example 5 and Example 6.
|
||||
To configure your connection using only keyword arguments, declare a name
|
||||
for the connection other than ``"snowflake"``.
|
||||
|
||||
For example, if your Snowflake account supports SSO, you can set up a quick
|
||||
local connection for development using `browser-based SSO
|
||||
<https://docs.snowflake.com/en/user-guide/admin-security-fed-auth-use#how-browser-based-sso-works>`_.
|
||||
Because there is nothing configured in ``secrets.toml``, the name is an
|
||||
empty string and the type is set to ``"snowflake"``. This prevents
|
||||
Streamlit from ignoring the keyword arguments and using a default
|
||||
Snowflake connection.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection(
|
||||
... "",
|
||||
... type="snowflake",
|
||||
... account="xxx-xxx",
|
||||
... user="xxx",
|
||||
... authenticator="externalbrowser",
|
||||
... )
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
|
||||
**Example 3: Named connection with Snowflake's connection configuration file**
|
||||
|
||||
Snowflake's Python Connector supports a `connection configuration file
|
||||
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-connect#connecting-using-the-connections-toml-file>`_,
|
||||
which is well integrated with Streamlit's ``SnowflakeConnection``. If you
|
||||
already have one or more connections configured, all you need to do is pass
|
||||
the name of the connection to use.
|
||||
|
||||
``~/.snowflake/connections.toml``:
|
||||
|
||||
>>> [my_connection]
|
||||
>>> account = "xxx-xxx"
|
||||
>>> user = "xxx"
|
||||
>>> password = "xxx"
|
||||
>>> warehouse = "xxx"
|
||||
>>> database = "xxx"
|
||||
>>> schema = "xxx"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection("my_connection", type="snowflake")
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
|
||||
**Example 4: Named connection with Streamlit secrets and Snowflake's connection configuration file**
|
||||
|
||||
If you have a Snowflake configuration file with a connection named
|
||||
``my_connection`` as in Example 3, you can pass the connection name through
|
||||
``secrets.toml``.
|
||||
|
||||
``.streamlit/secrets.toml``:
|
||||
|
||||
>>> [connections.snowflake]
|
||||
>>> connection_name = "my_connection"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
|
||||
**Example 5: Default connection with an environment variable**
|
||||
|
||||
If you don't have a ``[connections.snowflake]`` dictionary in your
|
||||
``secrets.toml`` file and use ``st.connection("snowflake")``, Streamlit
|
||||
will use the default connection for the `Snowflake Python Connector
|
||||
<https://docs.snowflake.cn/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection>`_.
|
||||
|
||||
If you have a Snowflake configuration file with a connection named
|
||||
``my_connection`` as in Example 3, you can set an environment variable to
|
||||
declare it as the default Snowflake connection.
|
||||
|
||||
>>> SNOWFLAKE_DEFAULT_CONNECTION_NAME = "my_connection"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
|
||||
**Example 6: Default connection in Snowflake's connection configuration file**
|
||||
|
||||
If you have a Snowflake configuration file that defines your ``default``
|
||||
connection, Streamlit will automatically use it if no other connection is
|
||||
declared.
|
||||
|
||||
``~/.snowflake/connections.toml``:
|
||||
|
||||
>>> [default]
|
||||
>>> account = "xxx-xxx"
|
||||
>>> user = "xxx"
|
||||
>>> password = "xxx"
|
||||
>>> warehouse = "xxx"
|
||||
>>> database = "xxx"
|
||||
>>> schema = "xxx"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
|
||||
"""
|
||||
|
||||
def _connect(self, **kwargs) -> InternalSnowflakeConnection:
|
||||
import snowflake.connector # type:ignore[import]
|
||||
from snowflake.connector import Error as SnowflakeError # type:ignore[import]
|
||||
|
||||
# If we're running in SiS, just call get_active_session() and retrieve the
|
||||
# lower-level connection from it.
|
||||
if running_in_sis():
|
||||
from snowflake.snowpark.context import ( # type:ignore[import] # isort: skip
|
||||
get_active_session,
|
||||
)
|
||||
|
||||
session = get_active_session()
|
||||
|
||||
if hasattr(session, "connection"):
|
||||
return session.connection
|
||||
# session.connection is only a valid attr in more recent versions of
|
||||
# snowflake-connector-python, so we fall back to grabbing
|
||||
# session._conn._conn if `.connection` is unavailable.
|
||||
return session._conn._conn
|
||||
|
||||
# We require qmark-style parameters everywhere for consistency across different
|
||||
# environments where SnowflakeConnections may be used.
|
||||
snowflake.connector.paramstyle = "qmark"
|
||||
|
||||
# Otherwise, attempt to create a new connection from whatever credentials we
|
||||
# have available.
|
||||
try:
|
||||
st_secrets = self._secrets.to_dict()
|
||||
if len(st_secrets):
|
||||
_LOGGER.info(
|
||||
"Connect to Snowflake using the Streamlit secret defined under "
|
||||
"[connections.snowflake]."
|
||||
)
|
||||
conn_kwargs = {**st_secrets, **kwargs}
|
||||
return snowflake.connector.connect(**conn_kwargs)
|
||||
|
||||
# Use the default configuration as defined in https://docs.snowflake.cn/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection
|
||||
if self._connection_name == "snowflake":
|
||||
_LOGGER.info(
|
||||
"Connect to Snowflake using the default configuration as defined "
|
||||
"in https://docs.snowflake.cn/en/developer-guide/python-connector/python-connector-connect#setting-a-default-connection"
|
||||
)
|
||||
return snowflake.connector.connect()
|
||||
|
||||
return snowflake.connector.connect(**kwargs)
|
||||
except SnowflakeError as e:
|
||||
if not len(st_secrets) and not kwargs:
|
||||
raise StreamlitAPIException(
|
||||
"Missing Snowflake connection configuration. "
|
||||
"Did you forget to set this in `secrets.toml`, a Snowflake configuration file, "
|
||||
"or as kwargs to `st.connection`? "
|
||||
"See the [SnowflakeConnection configuration documentation](https://docs.streamlit.io/st.connections.snowflakeconnection-configuration) "
|
||||
"for more details and examples."
|
||||
)
|
||||
raise e
|
||||
|
||||
def query(
|
||||
self,
|
||||
sql: str,
|
||||
*, # keyword-only arguments:
|
||||
ttl: float | int | timedelta | None = None,
|
||||
show_spinner: bool | str = "Running `snowflake.query(...)`.",
|
||||
params=None,
|
||||
**kwargs,
|
||||
) -> DataFrame:
|
||||
"""Run a read-only SQL query.
|
||||
|
||||
This method implements query result caching and simple error
|
||||
handling/retries. The caching behavior is identical to that of using
|
||||
``@st.cache_data``.
|
||||
|
||||
.. note::
|
||||
Queries that are run without a specified ``ttl`` are cached
|
||||
indefinitely.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sql : str
|
||||
The read-only SQL query to execute.
|
||||
ttl : float, int, timedelta or None
|
||||
The maximum number of seconds to keep results in the cache. If this
|
||||
is ``None`` (default), cached results do not expire with time.
|
||||
show_spinner : boolean or string
|
||||
Whether to enable the spinner. When a cached query is executed, no
|
||||
spinner is displayed because the result is immediately available.
|
||||
When a new query is executed, the default is to show a spinner with
|
||||
the message "Running ``snowflake.query(...)``."
|
||||
|
||||
If this is ``False``, no spinner displays while executing the
|
||||
query. If this is a string, the string will be used as the message
|
||||
for the spinner.
|
||||
params : list, tuple, dict or None
|
||||
List of parameters to pass to the Snowflake Connector for Python
|
||||
``Cursor.execute()`` method. This connector supports binding data
|
||||
to a SQL statement using qmark bindings. For more information and
|
||||
examples, see the `Snowflake Connector for Python documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-example#using-qmark-or-numeric-binding>`_.
|
||||
This defaults to ``None``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
The result of running the query, formatted as a pandas DataFrame.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> df = conn.query("SELECT * FROM my_table")
|
||||
>>> st.dataframe(df)
|
||||
|
||||
"""
|
||||
from tenacity import retry, retry_if_exception, stop_after_attempt, wait_fixed
|
||||
|
||||
# the ANSI-compliant SQL code for "connection was not established" (see docs: https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#id6)
|
||||
SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED = "08001"
|
||||
|
||||
@retry(
|
||||
after=lambda _: self.reset(),
|
||||
stop=stop_after_attempt(3),
|
||||
reraise=True,
|
||||
# We don't have to implement retries ourself for most error types as the
|
||||
# `snowflake-connector-python` library already implements retries for
|
||||
# retryable HTTP errors.
|
||||
retry=retry_if_exception(
|
||||
lambda e: hasattr(e, "sqlstate")
|
||||
and e.sqlstate == SQLSTATE_CONNECTION_WAS_NOT_ESTABLISHED
|
||||
),
|
||||
wait=wait_fixed(1),
|
||||
)
|
||||
def _query(sql: str) -> DataFrame:
|
||||
cur = self._instance.cursor()
|
||||
cur.execute(sql, params=params, **kwargs)
|
||||
return cur.fetch_pandas_all()
|
||||
|
||||
# We modify our helper function's `__qualname__` here to work around default
|
||||
# `@st.cache_data` behavior. Otherwise, `.query()` being called with different
|
||||
# `ttl` values will reset the cache with each call, and the query caches won't
|
||||
# be scoped by connection.
|
||||
ttl_str = str( # Avoid adding extra `.` characters to `__qualname__`
|
||||
ttl
|
||||
).replace(".", "_")
|
||||
_query.__qualname__ = f"{_query.__qualname__}_{self._connection_name}_{ttl_str}"
|
||||
_query = cache_data(
|
||||
show_spinner=show_spinner,
|
||||
ttl=ttl,
|
||||
)(_query)
|
||||
|
||||
return _query(sql)
|
||||
|
||||
def write_pandas(
|
||||
self,
|
||||
df: DataFrame,
|
||||
table_name: str,
|
||||
database: str | None = None,
|
||||
schema: str | None = None,
|
||||
chunk_size: int | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[bool, int, int]:
|
||||
"""Write a ``pandas.DataFrame`` to a table in a Snowflake database.
|
||||
|
||||
This convenience method is a thin wrapper around
|
||||
``snowflake.connector.pandas_tools.write_pandas()`` using the
|
||||
underlying connection. The ``conn`` parameter is passed automatically.
|
||||
For more information and additional keyword arguments, see the
|
||||
`Snowflake Connector for Python documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#write_pandas>`_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df: pandas.DataFrame
|
||||
The ``pandas.DataFrame`` object containing the data to be copied
|
||||
into the table.
|
||||
table_name: str
|
||||
Name of the table where the data should be copied to.
|
||||
database: str
|
||||
Name of the database containing the table. By default, the function
|
||||
writes to the database that is currently in use in the session.
|
||||
|
||||
.. Note::
|
||||
If you specify this parameter, you must also specify the schema
|
||||
parameter.
|
||||
|
||||
schema: str
|
||||
Name of the schema containing the table. By default, the function
|
||||
writes to the table in the schema that is currently in use in the
|
||||
session.
|
||||
chunk_size: int
|
||||
Number of elements to insert at a time. By default, the function
|
||||
inserts all elements in one chunk.
|
||||
**kwargs: Any
|
||||
Additional keyword arguments for
|
||||
``snowflake.connector.pandas_tools.write_pandas()``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[bool, int, int]
|
||||
A tuple containing three values:
|
||||
|
||||
1. A boolean value that is ``True`` if the write was successful.
|
||||
2. An integer giving the number of chunks of data that were copied.
|
||||
3. An integer giving the number of rows that were inserted.
|
||||
|
||||
Example
|
||||
-------
|
||||
The following example uses the database and schema currently in use in
|
||||
the session and copies the data into a table named "my_table."
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... {"Name": ["Mary", "John", "Robert"], "Pet": ["dog", "cat", "bird"]}
|
||||
... )
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> conn.write_pandas(df, "my_table")
|
||||
|
||||
"""
|
||||
from snowflake.connector.pandas_tools import write_pandas # type:ignore[import]
|
||||
|
||||
success, nchunks, nrows, _ = write_pandas(
|
||||
conn=self._instance,
|
||||
df=df,
|
||||
table_name=table_name,
|
||||
database=database,
|
||||
schema=schema,
|
||||
chunk_size=chunk_size,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
return (success, nchunks, nrows)
|
||||
|
||||
def cursor(self) -> SnowflakeCursor:
|
||||
"""Create a new cursor object from this connection.
|
||||
|
||||
Snowflake Connector cursors implement the Python Database API v2.0
|
||||
specification (PEP-249). For more information, see the
|
||||
`Snowflake Connector for Python documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#object-cursor>`_.
|
||||
|
||||
Returns
|
||||
-------
|
||||
snowflake.connector.cursor.SnowflakeCursor
|
||||
A cursor object for the connection.
|
||||
|
||||
Example
|
||||
-------
|
||||
The following example uses a cursor to insert multiple rows into a
|
||||
table. The ``qmark`` parameter style is specified as an optional
|
||||
keyword argument. Alternatively, the parameter style can be declared in
|
||||
your connection configuration file. For more information, see the
|
||||
`Snowflake Connector for Python documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-example#using-qmark-or-numeric-binding>`_.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("snowflake", "paramstyle"="qmark")
|
||||
>>> rows_to_insert = [("Mary", "dog"), ("John", "cat"), ("Robert", "bird")]
|
||||
>>> conn.cursor().executemany(
|
||||
... "INSERT INTO mytable (name, pet) VALUES (?, ?)", rows_to_insert
|
||||
... )
|
||||
|
||||
"""
|
||||
return self._instance.cursor()
|
||||
|
||||
@property
|
||||
def raw_connection(self) -> InternalSnowflakeConnection:
|
||||
"""Access the underlying connection object from the Snowflake\
|
||||
Connector for Python.
|
||||
|
||||
For information on how to use the Snowflake Connector for Python, see
|
||||
the `Snowflake Connector for Python documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-example>`_.
|
||||
|
||||
Returns
|
||||
-------
|
||||
snowflake.connector.connection.SnowflakeConnection
|
||||
The connection object.
|
||||
|
||||
Example
|
||||
-------
|
||||
The following example uses a cursor to submit an asynchronous query,
|
||||
saves the query ID, then periodically checks the query status through
|
||||
the connection before retrieving the results.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> cur = conn.cursor()
|
||||
>>> cur.execute_async("SELECT * FROM my_table")
|
||||
>>> query_id = cur.sfqid
|
||||
>>> while True:
|
||||
... status = conn.raw_connection.get_query_status(query_id)
|
||||
... if conn.raw_connection.is_still_running(status):
|
||||
... time.sleep(1)
|
||||
... else:
|
||||
... break
|
||||
>>> cur.get_results_from_sfqid(query_id)
|
||||
>>> df = cur.fetchall()
|
||||
|
||||
"""
|
||||
return self._instance
|
||||
|
||||
def session(self) -> Session:
|
||||
"""Create a new Snowpark session from this connection.
|
||||
|
||||
For information on how to use Snowpark sessions, see the
|
||||
`Snowpark developer guide
|
||||
<https://docs.snowflake.com/en/developer-guide/snowpark/python/working-with-dataframes>`_
|
||||
and `Snowpark API Reference
|
||||
<https://docs.snowflake.com/en/developer-guide/snowpark/reference/python/latest/snowpark/session>`_.
|
||||
|
||||
Returns
|
||||
-------
|
||||
snowflake.snowpark.Session
|
||||
A new Snowpark session for this connection.
|
||||
|
||||
Example
|
||||
-------
|
||||
The following example creates a new Snowpark session and uses it to run
|
||||
a query.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("snowflake")
|
||||
>>> session = conn.session()
|
||||
>>> df = session.sql("SELECT * FROM my_table").collect()
|
||||
|
||||
"""
|
||||
from snowflake.snowpark.context import get_active_session # type:ignore[import]
|
||||
from snowflake.snowpark.session import Session # type:ignore[import]
|
||||
|
||||
if running_in_sis():
|
||||
return get_active_session()
|
||||
|
||||
return cast(
|
||||
"Session", Session.builder.configs({"connection": self._instance}).create()
|
||||
)
|
||||
@@ -0,0 +1,213 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# NOTE: We won't always be able to import from snowflake.snowpark.session so need the
|
||||
# `type: ignore` comment below, but that comment will explode if `warn-unused-ignores` is
|
||||
# turned on when the package is available. Unfortunately, mypy doesn't provide a good
|
||||
# way to configure this at a per-line level :(
|
||||
# mypy: no-warn-unused-ignores
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections import ChainMap
|
||||
from contextlib import contextmanager
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.connections import BaseConnection
|
||||
from streamlit.connections.util import (
|
||||
SNOWSQL_CONNECTION_FILE,
|
||||
load_from_snowsql_config_file,
|
||||
running_in_sis,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.caching import cache_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
|
||||
from pandas import DataFrame
|
||||
from snowflake.snowpark.session import Session # type:ignore[import]
|
||||
|
||||
|
||||
_REQUIRED_CONNECTION_PARAMS = {"account"}
|
||||
|
||||
|
||||
class SnowparkConnection(BaseConnection["Session"]):
|
||||
"""A connection to Snowpark using snowflake.snowpark.session.Session. Initialize using
|
||||
``st.connection("<name>", type="snowpark")``.
|
||||
|
||||
In addition to providing access to the Snowpark Session, SnowparkConnection supports
|
||||
direct SQL querying using ``query("...")`` and thread safe access using
|
||||
``with conn.safe_session():``. See methods below for more information.
|
||||
SnowparkConnections should always be created using ``st.connection()``, **not**
|
||||
initialized directly.
|
||||
|
||||
.. note::
|
||||
We don't expect this iteration of SnowparkConnection to be able to scale
|
||||
well in apps with many concurrent users due to the lock contention that will occur
|
||||
over the single underlying Session object under high load.
|
||||
"""
|
||||
|
||||
def __init__(self, connection_name: str, **kwargs) -> None:
|
||||
self._lock = threading.RLock()
|
||||
super().__init__(connection_name, **kwargs)
|
||||
|
||||
def _connect(self, **kwargs) -> Session:
|
||||
from snowflake.snowpark.context import get_active_session # type:ignore[import]
|
||||
from snowflake.snowpark.session import Session
|
||||
|
||||
# If we're running in SiS, just call get_active_session(). Otherwise, attempt to
|
||||
# create a new session from whatever credentials we have available.
|
||||
if running_in_sis():
|
||||
return get_active_session()
|
||||
|
||||
conn_params = ChainMap(
|
||||
kwargs,
|
||||
self._secrets.to_dict(),
|
||||
load_from_snowsql_config_file(self._connection_name),
|
||||
)
|
||||
|
||||
if not len(conn_params):
|
||||
raise StreamlitAPIException(
|
||||
"Missing Snowpark connection configuration. "
|
||||
f"Did you forget to set this in `secrets.toml`, `{SNOWSQL_CONNECTION_FILE}`, "
|
||||
"or as kwargs to `st.connection`?"
|
||||
)
|
||||
|
||||
for p in _REQUIRED_CONNECTION_PARAMS:
|
||||
if p not in conn_params:
|
||||
raise StreamlitAPIException(f"Missing Snowpark connection param: {p}")
|
||||
|
||||
return cast("Session", Session.builder.configs(conn_params).create())
|
||||
|
||||
def query(
|
||||
self,
|
||||
sql: str,
|
||||
ttl: float | int | timedelta | None = None,
|
||||
) -> DataFrame:
|
||||
"""Run a read-only SQL query.
|
||||
|
||||
This method implements both query result caching (with caching behavior
|
||||
identical to that of using ``@st.cache_data``) as well as simple error handling/retries.
|
||||
|
||||
.. note::
|
||||
Queries that are run without a specified ttl are cached indefinitely.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sql : str
|
||||
The read-only SQL query to execute.
|
||||
ttl : float, int, timedelta or None
|
||||
The maximum number of seconds to keep results in the cache, or
|
||||
None if cached results should not expire. The default is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
The result of running the query, formatted as a pandas DataFrame.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("snowpark")
|
||||
>>> df = conn.query("SELECT * FROM pet_owners")
|
||||
>>> st.dataframe(df)
|
||||
"""
|
||||
from snowflake.snowpark.exceptions import ( # type:ignore[import]
|
||||
SnowparkServerException,
|
||||
)
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_fixed,
|
||||
)
|
||||
|
||||
@retry(
|
||||
after=lambda _: self.reset(),
|
||||
stop=stop_after_attempt(3),
|
||||
reraise=True,
|
||||
retry=retry_if_exception_type(SnowparkServerException),
|
||||
wait=wait_fixed(1),
|
||||
)
|
||||
def _query(sql: str) -> DataFrame:
|
||||
with self._lock:
|
||||
return self._instance.sql(sql).to_pandas()
|
||||
|
||||
# We modify our helper function's `__qualname__` here to work around default
|
||||
# `@st.cache_data` behavior. Otherwise, `.query()` being called with different
|
||||
# `ttl` values will reset the cache with each call, and the query caches won't
|
||||
# be scoped by connection.
|
||||
ttl_str = str( # Avoid adding extra `.` characters to `__qualname__`
|
||||
ttl
|
||||
).replace(".", "_")
|
||||
_query.__qualname__ = f"{_query.__qualname__}_{self._connection_name}_{ttl_str}"
|
||||
_query = cache_data(
|
||||
show_spinner="Running `snowpark.query(...)`.",
|
||||
ttl=ttl,
|
||||
)(_query)
|
||||
|
||||
return _query(sql)
|
||||
|
||||
@property
|
||||
def session(self) -> Session:
|
||||
"""Access the underlying Snowpark session.
|
||||
|
||||
.. note::
|
||||
Snowpark sessions are **not** thread safe. Users of this method are
|
||||
responsible for ensuring that access to the session returned by this method is
|
||||
done in a thread-safe manner. For most users, we recommend using the thread-safe
|
||||
safe_session() method and a ``with`` block.
|
||||
|
||||
Information on how to use Snowpark sessions can be found in the `Snowpark documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/snowpark/python/working-with-dataframes>`_.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> session = st.connection("snowpark").session
|
||||
>>> df = session.table("mytable").limit(10).to_pandas()
|
||||
>>> st.dataframe(df)
|
||||
"""
|
||||
return self._instance
|
||||
|
||||
@contextmanager
|
||||
def safe_session(self) -> Iterator[Session]:
|
||||
"""Grab the underlying Snowpark session in a thread-safe manner.
|
||||
|
||||
As operations on a Snowpark session are not thread safe, we need to take care
|
||||
when using a session in the context of a Streamlit app where each script run
|
||||
occurs in its own thread. Using the contextmanager pattern to do this ensures
|
||||
that access on this connection's underlying Session is done in a thread-safe
|
||||
manner.
|
||||
|
||||
Information on how to use Snowpark sessions can be found in the `Snowpark documentation
|
||||
<https://docs.snowflake.com/en/developer-guide/snowpark/python/working-with-dataframes>`_.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("snowpark")
|
||||
>>> with conn.safe_session() as session:
|
||||
... df = session.table("mytable").limit(10).to_pandas()
|
||||
>>>
|
||||
>>> st.dataframe(df)
|
||||
"""
|
||||
with self._lock:
|
||||
yield self.session
|
||||
@@ -0,0 +1,424 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# NOTE: We ignore all mypy import-not-found errors as top-level since
|
||||
# this module is optional and the SQLAlchemy dependency is not installed
|
||||
# by default.
|
||||
# mypy: disable-error-code="import-not-found, redundant-cast"
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import ChainMap
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.connections import BaseConnection
|
||||
from streamlit.connections.util import extract_from_dict
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.caching import cache_data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import timedelta
|
||||
|
||||
from pandas import DataFrame
|
||||
from sqlalchemy.engine import Connection as SQLAlchemyConnection
|
||||
from sqlalchemy.engine.base import Engine
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
_ALL_CONNECTION_PARAMS = {
|
||||
"url",
|
||||
"driver",
|
||||
"dialect",
|
||||
"username",
|
||||
"password",
|
||||
"host",
|
||||
"port",
|
||||
"database",
|
||||
"query",
|
||||
}
|
||||
_REQUIRED_CONNECTION_PARAMS = {"dialect", "username", "host"}
|
||||
|
||||
|
||||
class SQLConnection(BaseConnection["Engine"]):
|
||||
"""A connection to a SQL database using a SQLAlchemy Engine.
|
||||
|
||||
Initialize this connection object using ``st.connection("sql")`` or
|
||||
``st.connection("<name>", type="sql")``. Connection parameters for a
|
||||
SQLConnection can be specified using ``secrets.toml`` and/or ``**kwargs``.
|
||||
Possible connection parameters include:
|
||||
|
||||
- ``url`` or keyword arguments for |sqlalchemy.engine.URL.create()|_, except
|
||||
``drivername``. Use ``dialect`` and ``driver`` instead of ``drivername``.
|
||||
- Keyword arguments for |sqlalchemy.create_engine()|_, including custom
|
||||
``connect()`` arguments used by your specific ``dialect`` or ``driver``.
|
||||
- ``autocommit``. If this is ``False`` (default), the connection operates
|
||||
in manual commit (transactional) mode. If this is ``True``, the
|
||||
connection operates in autocommit (non-transactional) mode.
|
||||
|
||||
If ``url`` exists as a connection parameter, Streamlit will pass it to
|
||||
``sqlalchemy.engine.make_url()``. Otherwise, Streamlit requires (at a
|
||||
minimum) ``dialect``, ``username``, and ``host``. Streamlit will use
|
||||
``dialect`` and ``driver`` (if defined) to derive ``drivername``, then pass
|
||||
the relevant connection parameters to ``sqlalchemy.engine.URL.create()``.
|
||||
|
||||
In addition to the default keyword arguments for ``sqlalchemy.create_engine()``,
|
||||
your dialect may accept additional keyword arguments. For example, if you
|
||||
use ``dialect="snowflake"`` with `Snowflake SQLAlchemy
|
||||
<https://github.com/snowflakedb/snowflake-sqlalchemy#key-pair-authentication-support>`_,
|
||||
you can pass a value for ``private_key`` to use key-pair authentication. If
|
||||
you use ``dialect="bigquery"`` with `Google BigQuery
|
||||
<https://github.com/googleapis/python-bigquery-sqlalchemy#authentication>`_,
|
||||
you can pass a value for ``location``.
|
||||
|
||||
SQLConnection provides the ``.query()`` convenience method, which can be
|
||||
used to run simple, read-only queries with both caching and simple error
|
||||
handling/retries. More complex database interactions can be performed by
|
||||
using the ``.session`` property to receive a regular SQLAlchemy Session.
|
||||
|
||||
.. Important::
|
||||
`SQLAlchemy <https://pypi.org/project/SQLAlchemy/>`_ must be installed
|
||||
in your environment to use this connection. You must also install your
|
||||
driver, such as ``pyodbc`` or ``psycopg2``.
|
||||
|
||||
.. |sqlalchemy.engine.URL.create()| replace:: ``sqlalchemy.engine.URL.create()``
|
||||
.. _sqlalchemy.engine.URL.create(): https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.engine.URL.create
|
||||
.. |sqlalchemy.engine.make_url()| replace:: ``sqlalchemy.engine.make_url()``
|
||||
.. _sqlalchemy.engine.make_url(): https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.engine.make_url
|
||||
.. |sqlalchemy.create_engine()| replace:: ``sqlalchemy.create_engine()``
|
||||
.. _sqlalchemy.create_engine(): https://docs.sqlalchemy.org/en/20/core/engines.html#sqlalchemy.create_engine
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Configuration with URL**
|
||||
|
||||
You can configure your SQL connection using Streamlit's
|
||||
`Secrets management <https://docs.streamlit.io/develop/concepts/connections/secrets-management>`_.
|
||||
The following example specifies a SQL connection URL.
|
||||
|
||||
``.streamlit/secrets.toml``:
|
||||
|
||||
>>> [connections.sql]
|
||||
>>> url = "xxx+xxx://xxx:xxx@xxx:xxx/xxx"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("sql")
|
||||
>>> df = conn.query("SELECT * FROM pet_owners")
|
||||
>>> st.dataframe(df)
|
||||
|
||||
**Example 2: Configuration with dialect, host, and username**
|
||||
|
||||
If you do not specify ``url``, you must at least specify ``dialect``,
|
||||
``host``, and ``username`` instead. The following example also includes
|
||||
``password``.
|
||||
|
||||
``.streamlit/secrets.toml``:
|
||||
|
||||
>>> [connections.sql]
|
||||
>>> dialect = "xxx"
|
||||
>>> host = "xxx"
|
||||
>>> username = "xxx"
|
||||
>>> password = "xxx"
|
||||
|
||||
Your app code:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("sql")
|
||||
>>> df = conn.query("SELECT * FROM pet_owners")
|
||||
>>> st.dataframe(df)
|
||||
|
||||
**Example 3: Configuration with keyword arguments**
|
||||
|
||||
You can configure your SQL connection with keyword arguments (with or
|
||||
without ``secrets.toml``). For example, if you use Microsoft Entra ID with
|
||||
a Microsoft Azure SQL server, you can quickly set up a local connection for
|
||||
development using `interactive authentication
|
||||
<https://learn.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory?view=sql-server-ver16#new-andor-modified-dsn-and-connection-string-keywords>`_.
|
||||
|
||||
This example requires the `Microsoft ODBC Driver for SQL Server
|
||||
<https://learn.microsoft.com/en-us/sql/connect/odbc/microsoft-odbc-driver-for-sql-server?view=sql-server-ver16>`_
|
||||
for *Windows* in addition to the ``sqlalchemy`` and ``pyodbc`` packages for
|
||||
Python.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection(
|
||||
... "sql",
|
||||
... dialect="mssql",
|
||||
... driver="pyodbc",
|
||||
... host="xxx.database.windows.net",
|
||||
... database="xxx",
|
||||
... username="xxx",
|
||||
... query={
|
||||
... "driver": "ODBC Driver 18 for SQL Server",
|
||||
... "authentication": "ActiveDirectoryInteractive",
|
||||
... "encrypt": "yes",
|
||||
... },
|
||||
... )
|
||||
>>>
|
||||
>>> df = conn.query("SELECT * FROM pet_owners")
|
||||
>>> st.dataframe(df)
|
||||
|
||||
"""
|
||||
|
||||
def _connect(self, autocommit: bool = False, **kwargs) -> Engine:
|
||||
import sqlalchemy
|
||||
|
||||
kwargs = deepcopy(kwargs)
|
||||
conn_param_kwargs = extract_from_dict(_ALL_CONNECTION_PARAMS, kwargs)
|
||||
conn_params = ChainMap(conn_param_kwargs, self._secrets.to_dict())
|
||||
|
||||
if not len(conn_params):
|
||||
raise StreamlitAPIException(
|
||||
"Missing SQL DB connection configuration. "
|
||||
"Did you forget to set this in `secrets.toml` or as kwargs to `st.connection`?"
|
||||
)
|
||||
|
||||
if "url" in conn_params:
|
||||
url = sqlalchemy.engine.make_url(conn_params["url"])
|
||||
else:
|
||||
for p in _REQUIRED_CONNECTION_PARAMS:
|
||||
if p not in conn_params:
|
||||
raise StreamlitAPIException(f"Missing SQL DB connection param: {p}")
|
||||
|
||||
drivername = conn_params["dialect"] + (
|
||||
f"+{conn_params['driver']}" if "driver" in conn_params else ""
|
||||
)
|
||||
|
||||
url = sqlalchemy.engine.URL.create(
|
||||
drivername=drivername,
|
||||
username=conn_params["username"],
|
||||
password=conn_params.get("password"),
|
||||
host=conn_params["host"],
|
||||
port=int(conn_params["port"]) if "port" in conn_params else None,
|
||||
database=conn_params.get("database"),
|
||||
query=conn_params["query"] if "query" in conn_params else None,
|
||||
)
|
||||
|
||||
create_engine_kwargs = ChainMap(
|
||||
kwargs, self._secrets.get("create_engine_kwargs", {})
|
||||
)
|
||||
eng = sqlalchemy.create_engine(url, **create_engine_kwargs)
|
||||
|
||||
if autocommit:
|
||||
return cast("Engine", eng.execution_options(isolation_level="AUTOCOMMIT"))
|
||||
else:
|
||||
return cast("Engine", eng)
|
||||
|
||||
def query(
|
||||
self,
|
||||
sql: str,
|
||||
*, # keyword-only arguments:
|
||||
show_spinner: bool | str = "Running `sql.query(...)`.",
|
||||
ttl: float | int | timedelta | None = None,
|
||||
index_col: str | list[str] | None = None,
|
||||
chunksize: int | None = None,
|
||||
params=None,
|
||||
**kwargs,
|
||||
) -> DataFrame:
|
||||
"""Run a read-only query.
|
||||
|
||||
This method implements query result caching and simple error
|
||||
handling/retries. The caching behavior is identical to that of using
|
||||
``@st.cache_data``.
|
||||
|
||||
.. note::
|
||||
Queries that are run without a specified ttl are cached indefinitely.
|
||||
|
||||
All keyword arguments passed to this function are passed down to
|
||||
|pandas.read_sql|_, except ``ttl``.
|
||||
|
||||
.. |pandas.read_sql| replace:: ``pandas.read_sql``
|
||||
.. _pandas.read_sql: https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html
|
||||
|
||||
Parameters
|
||||
----------
|
||||
sql : str
|
||||
The read-only SQL query to execute.
|
||||
show_spinner : boolean or string
|
||||
Enable the spinner. The default is to show a spinner when there is a
|
||||
"cache miss" and the cached resource is being created. If a string, the value
|
||||
of the show_spinner param will be used for the spinner text.
|
||||
ttl : float, int, timedelta or None
|
||||
The maximum number of seconds to keep results in the cache, or
|
||||
None if cached results should not expire. The default is None.
|
||||
index_col : str, list of str, or None
|
||||
Column(s) to set as index(MultiIndex). Default is None.
|
||||
chunksize : int or None
|
||||
If specified, return an iterator where chunksize is the number of
|
||||
rows to include in each chunk. Default is None.
|
||||
params : list, tuple, dict or None
|
||||
List of parameters to pass to the execute method. The syntax used to pass
|
||||
parameters is database driver dependent. Check your database driver
|
||||
documentation for which of the five syntax styles, described in `PEP 249
|
||||
paramstyle <https://peps.python.org/pep-0249/#paramstyle>`_, is supported.
|
||||
Default is None.
|
||||
**kwargs: dict
|
||||
Additional keyword arguments are passed to |pandas.read_sql|_.
|
||||
|
||||
.. |pandas.read_sql| replace:: ``pandas.read_sql``
|
||||
.. _pandas.read_sql: https://pandas.pydata.org/docs/reference/api/pandas.read_sql.html
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
The result of running the query, formatted as a pandas DataFrame.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> conn = st.connection("sql")
|
||||
>>> df = conn.query(
|
||||
... "SELECT * FROM pet_owners WHERE owner = :owner",
|
||||
... ttl=3600,
|
||||
... params={"owner": "barbara"},
|
||||
... )
|
||||
>>> st.dataframe(df)
|
||||
"""
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.exc import DatabaseError, InternalError, OperationalError
|
||||
from tenacity import (
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_fixed,
|
||||
)
|
||||
|
||||
@retry(
|
||||
after=lambda _: self.reset(),
|
||||
stop=stop_after_attempt(3),
|
||||
reraise=True,
|
||||
retry=retry_if_exception_type(
|
||||
(DatabaseError, InternalError, OperationalError)
|
||||
),
|
||||
wait=wait_fixed(1),
|
||||
)
|
||||
def _query(
|
||||
sql: str,
|
||||
index_col=None,
|
||||
chunksize=None,
|
||||
params=None,
|
||||
**kwargs,
|
||||
) -> DataFrame:
|
||||
import pandas as pd
|
||||
|
||||
instance = self._instance.connect()
|
||||
return pd.read_sql(
|
||||
text(sql),
|
||||
instance,
|
||||
index_col=index_col,
|
||||
chunksize=chunksize,
|
||||
params=params,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# We modify our helper function's `__qualname__` here to work around default
|
||||
# `@st.cache_data` behavior. Otherwise, `.query()` being called with different
|
||||
# `ttl` values will reset the cache with each call, and the query caches won't
|
||||
# be scoped by connection.
|
||||
ttl_str = str( # Avoid adding extra `.` characters to `__qualname__`
|
||||
ttl
|
||||
).replace(".", "_")
|
||||
_query.__qualname__ = f"{_query.__qualname__}_{self._connection_name}_{ttl_str}"
|
||||
_query = cache_data(
|
||||
show_spinner=show_spinner,
|
||||
ttl=ttl,
|
||||
)(_query)
|
||||
|
||||
return _query(
|
||||
sql,
|
||||
index_col=index_col,
|
||||
chunksize=chunksize,
|
||||
params=params,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def connect(self) -> SQLAlchemyConnection:
|
||||
"""Call ``.connect()`` on the underlying SQLAlchemy Engine, returning a new\
|
||||
connection object.
|
||||
|
||||
Calling this method is equivalent to calling ``self._instance.connect()``.
|
||||
|
||||
NOTE: This method should not be confused with the internal ``_connect`` method used
|
||||
to implement a Streamlit Connection.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sqlalchemy.engine.Connection
|
||||
A new SQLAlchemy connection object.
|
||||
"""
|
||||
return self._instance.connect()
|
||||
|
||||
@property
|
||||
def engine(self) -> Engine:
|
||||
"""The underlying SQLAlchemy Engine.
|
||||
|
||||
This is equivalent to accessing ``self._instance``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sqlalchemy.engine.base.Engine
|
||||
The underlying SQLAlchemy Engine.
|
||||
"""
|
||||
return self._instance
|
||||
|
||||
@property
|
||||
def driver(self) -> str:
|
||||
"""The name of the driver used by the underlying SQLAlchemy Engine.
|
||||
|
||||
This is equivalent to accessing ``self._instance.driver``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The name of the driver. For example, ``"pyodbc"`` or ``"psycopg2"``.
|
||||
"""
|
||||
return cast("str", self._instance.driver)
|
||||
|
||||
@property
|
||||
def session(self) -> Session:
|
||||
"""Return a SQLAlchemy Session.
|
||||
|
||||
Users of this connection should use the contextmanager pattern for writes,
|
||||
transactions, and anything more complex than simple read queries.
|
||||
|
||||
See the usage example below, which assumes we have a table ``numbers`` with a
|
||||
single integer column ``val``. The `SQLAlchemy
|
||||
<https://docs.sqlalchemy.org/en/20/orm/session_basics.html>`_ docs also contain
|
||||
much more information on the usage of sessions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sqlalchemy.orm.Session
|
||||
A SQLAlchemy Session.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> conn = st.connection("sql")
|
||||
>>> n = st.slider("Pick a number")
|
||||
>>> if st.button("Add the number!"):
|
||||
... with conn.session as session:
|
||||
... session.execute("INSERT INTO numbers (val) VALUES (:n);", {"n": n})
|
||||
... session.commit()
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
return Session(self._instance)
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# NOTE: We won't always be able to import from snowflake.connector.connection so need the
|
||||
# `type: ignore` comment below, but that comment will explode if `warn-unused-ignores` is
|
||||
# turned on when the package is available. Unfortunately, mypy doesn't provide a good
|
||||
# way to configure this at a per-line level :(
|
||||
# mypy: no-warn-unused-ignores
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Collection
|
||||
|
||||
SNOWSQL_CONNECTION_FILE = "~/.snowsql/config"
|
||||
|
||||
|
||||
def extract_from_dict(
|
||||
keys: Collection[str], source_dict: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Extract the specified keys from source_dict and return them in a new dict.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
keys : Collection[str]
|
||||
The keys to extract from source_dict.
|
||||
source_dict : Dict[str, Any]
|
||||
The dict to extract keys from. Note that this function mutates source_dict.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, Any]
|
||||
A new dict containing the keys/values extracted from source_dict.
|
||||
"""
|
||||
d = {}
|
||||
|
||||
for k in keys:
|
||||
if k in source_dict:
|
||||
d[k] = source_dict.pop(k)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def load_from_snowsql_config_file(connection_name: str) -> dict[str, Any]:
|
||||
"""Loads the dictionary from snowsql config file."""
|
||||
snowsql_config_file = os.path.expanduser(SNOWSQL_CONNECTION_FILE)
|
||||
if not os.path.exists(snowsql_config_file):
|
||||
return {}
|
||||
|
||||
# Lazy-load config parser for better import / startup performance
|
||||
import configparser
|
||||
|
||||
config = configparser.ConfigParser(inline_comment_prefixes="#")
|
||||
config.read(snowsql_config_file)
|
||||
|
||||
if f"connections.{connection_name}" in config:
|
||||
raw_conn_params = config[f"connections.{connection_name}"]
|
||||
elif "connections" in config:
|
||||
raw_conn_params = config["connections"]
|
||||
else:
|
||||
return {}
|
||||
|
||||
conn_params = {
|
||||
k.replace("name", ""): v.strip('"') for k, v in raw_conn_params.items()
|
||||
}
|
||||
|
||||
if "db" in conn_params:
|
||||
conn_params["database"] = conn_params["db"]
|
||||
del conn_params["db"]
|
||||
|
||||
return conn_params
|
||||
|
||||
|
||||
def running_in_sis() -> bool:
|
||||
"""Return whether this app is running in SiS."""
|
||||
try:
|
||||
from snowflake.snowpark._internal.utils import ( # type: ignore[import] # isort: skip
|
||||
is_in_stored_procedure,
|
||||
)
|
||||
|
||||
return cast("bool", is_in_stored_procedure())
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
210
myenv/lib/python3.11/site-packages/streamlit/cursor.py
Normal file
210
myenv/lib/python3.11/site-packages/streamlit/cursor.py
Normal file
@@ -0,0 +1,210 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from streamlit import util
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
|
||||
|
||||
def make_delta_path(
|
||||
root_container: int, parent_path: tuple[int, ...], index: int
|
||||
) -> list[int]:
|
||||
delta_path = [root_container]
|
||||
delta_path.extend(parent_path)
|
||||
delta_path.append(index)
|
||||
return delta_path
|
||||
|
||||
|
||||
def get_container_cursor(
|
||||
root_container: int | None,
|
||||
) -> RunningCursor | None:
|
||||
"""Return the top-level RunningCursor for the given container.
|
||||
This is the cursor that is used when user code calls something like
|
||||
`st.foo` (which uses the main container) or `st.sidebar.foo` (which uses
|
||||
the sidebar container).
|
||||
"""
|
||||
if root_container is None:
|
||||
return None
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if ctx is None:
|
||||
return None
|
||||
|
||||
if root_container in ctx.cursors:
|
||||
return ctx.cursors[root_container]
|
||||
|
||||
cursor = RunningCursor(root_container=root_container)
|
||||
ctx.cursors[root_container] = cursor
|
||||
return cursor
|
||||
|
||||
|
||||
class Cursor:
|
||||
"""A pointer to a delta location in the app.
|
||||
|
||||
When adding an element to the app, you should always call
|
||||
get_locked_cursor() on that element's respective Cursor.
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return util.repr_(self)
|
||||
|
||||
@property
|
||||
def root_container(self) -> int:
|
||||
"""The top-level container this cursor lives within - either
|
||||
RootContainer.MAIN or RootContainer.SIDEBAR.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def parent_path(self) -> tuple[int, ...]:
|
||||
"""The cursor's parent's path within its container."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
"""The index of the Delta within its parent block."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def delta_path(self) -> list[int]:
|
||||
"""The complete path of the delta pointed to by this cursor - its
|
||||
container, parent path, and index.
|
||||
"""
|
||||
return make_delta_path(self.root_container, self.parent_path, self.index)
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_locked_cursor(self, **props) -> LockedCursor:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def props(self) -> Any:
|
||||
"""Other data in this cursor. This is a temporary measure that will go
|
||||
away when we implement improved return values for elements.
|
||||
|
||||
This is only implemented in LockedCursor.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class RunningCursor(Cursor):
|
||||
def __init__(self, root_container: int, parent_path: tuple[int, ...] = ()):
|
||||
"""A moving pointer to a delta location in the app.
|
||||
|
||||
RunningCursors auto-increment to the next available location when you
|
||||
call get_locked_cursor() on them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
root_container: int
|
||||
The root container this cursor lives in.
|
||||
parent_path: tuple of ints
|
||||
The full path of this cursor, consisting of the IDs of all ancestors.
|
||||
The 0th item is the topmost ancestor.
|
||||
|
||||
"""
|
||||
self._root_container = root_container
|
||||
self._parent_path = parent_path
|
||||
self._index = 0
|
||||
|
||||
@property
|
||||
def root_container(self) -> int:
|
||||
return self._root_container
|
||||
|
||||
@property
|
||||
def parent_path(self) -> tuple[int, ...]:
|
||||
return self._parent_path
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
return False
|
||||
|
||||
def get_locked_cursor(self, **props) -> LockedCursor:
|
||||
locked_cursor = LockedCursor(
|
||||
root_container=self._root_container,
|
||||
parent_path=self._parent_path,
|
||||
index=self._index,
|
||||
**props,
|
||||
)
|
||||
|
||||
self._index += 1
|
||||
|
||||
return locked_cursor
|
||||
|
||||
|
||||
class LockedCursor(Cursor):
|
||||
def __init__(
|
||||
self,
|
||||
root_container: int,
|
||||
parent_path: tuple[int, ...] = (),
|
||||
index: int = 0,
|
||||
**props,
|
||||
):
|
||||
"""A locked pointer to a location in the app.
|
||||
|
||||
LockedCursors always point to the same location, even when you call
|
||||
get_locked_cursor() on them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
root_container: int
|
||||
The root container this cursor lives in.
|
||||
parent_path: tuple of ints
|
||||
The full path of this cursor, consisting of the IDs of all ancestors. The
|
||||
0th item is the topmost ancestor.
|
||||
index: int
|
||||
**props: any
|
||||
Anything else you want to store in this cursor. This is a temporary
|
||||
measure that will go away when we implement improved return values
|
||||
for elements.
|
||||
|
||||
"""
|
||||
self._root_container = root_container
|
||||
self._index = index
|
||||
self._parent_path = parent_path
|
||||
self._props = props
|
||||
|
||||
@property
|
||||
def root_container(self) -> int:
|
||||
return self._root_container
|
||||
|
||||
@property
|
||||
def parent_path(self) -> tuple[int, ...]:
|
||||
return self._parent_path
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def is_locked(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_locked_cursor(self, **props) -> LockedCursor:
|
||||
self._props = props
|
||||
return self
|
||||
|
||||
@property
|
||||
def props(self) -> Any:
|
||||
return self._props
|
||||
1417
myenv/lib/python3.11/site-packages/streamlit/dataframe_util.py
Normal file
1417
myenv/lib/python3.11/site-packages/streamlit/dataframe_util.py
Normal file
File diff suppressed because it is too large
Load Diff
602
myenv/lib/python3.11/site-packages/streamlit/delta_generator.py
Normal file
602
myenv/lib/python3.11/site-packages/streamlit/delta_generator.py
Normal file
@@ -0,0 +1,602 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Allows us to create and absorb changes (aka Deltas) to elements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from copy import deepcopy
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Final,
|
||||
Literal,
|
||||
NoReturn,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import (
|
||||
cli_util,
|
||||
config,
|
||||
cursor,
|
||||
env_util,
|
||||
logger,
|
||||
runtime,
|
||||
util,
|
||||
)
|
||||
from streamlit.delta_generator_singletons import (
|
||||
context_dg_stack,
|
||||
get_last_dg_added_to_context_stack,
|
||||
)
|
||||
from streamlit.elements.alert import AlertMixin
|
||||
from streamlit.elements.arrow import ArrowMixin
|
||||
from streamlit.elements.balloons import BalloonsMixin
|
||||
from streamlit.elements.bokeh_chart import BokehMixin
|
||||
from streamlit.elements.code import CodeMixin
|
||||
from streamlit.elements.deck_gl_json_chart import PydeckMixin
|
||||
from streamlit.elements.doc_string import HelpMixin
|
||||
from streamlit.elements.empty import EmptyMixin
|
||||
from streamlit.elements.exception import ExceptionMixin
|
||||
from streamlit.elements.form import FormMixin
|
||||
from streamlit.elements.graphviz_chart import GraphvizMixin
|
||||
from streamlit.elements.heading import HeadingMixin
|
||||
from streamlit.elements.html import HtmlMixin
|
||||
from streamlit.elements.iframe import IframeMixin
|
||||
from streamlit.elements.image import ImageMixin
|
||||
from streamlit.elements.json import JsonMixin
|
||||
from streamlit.elements.layouts import LayoutsMixin
|
||||
from streamlit.elements.lib.form_utils import FormData, current_form_id
|
||||
from streamlit.elements.map import MapMixin
|
||||
from streamlit.elements.markdown import MarkdownMixin
|
||||
from streamlit.elements.media import MediaMixin
|
||||
from streamlit.elements.metric import MetricMixin
|
||||
from streamlit.elements.plotly_chart import PlotlyMixin
|
||||
from streamlit.elements.progress import ProgressMixin
|
||||
from streamlit.elements.pyplot import PyplotMixin
|
||||
from streamlit.elements.snow import SnowMixin
|
||||
from streamlit.elements.text import TextMixin
|
||||
from streamlit.elements.toast import ToastMixin
|
||||
from streamlit.elements.vega_charts import VegaChartsMixin
|
||||
from streamlit.elements.widgets.audio_input import AudioInputMixin
|
||||
from streamlit.elements.widgets.button import ButtonMixin
|
||||
from streamlit.elements.widgets.button_group import ButtonGroupMixin
|
||||
from streamlit.elements.widgets.camera_input import CameraInputMixin
|
||||
from streamlit.elements.widgets.chat import ChatMixin
|
||||
from streamlit.elements.widgets.checkbox import CheckboxMixin
|
||||
from streamlit.elements.widgets.color_picker import ColorPickerMixin
|
||||
from streamlit.elements.widgets.data_editor import DataEditorMixin
|
||||
from streamlit.elements.widgets.file_uploader import FileUploaderMixin
|
||||
from streamlit.elements.widgets.multiselect import MultiSelectMixin
|
||||
from streamlit.elements.widgets.number_input import NumberInputMixin
|
||||
from streamlit.elements.widgets.radio import RadioMixin
|
||||
from streamlit.elements.widgets.select_slider import SelectSliderMixin
|
||||
from streamlit.elements.widgets.selectbox import SelectboxMixin
|
||||
from streamlit.elements.widgets.slider import SliderMixin
|
||||
from streamlit.elements.widgets.text_widgets import TextWidgetsMixin
|
||||
from streamlit.elements.widgets.time_widgets import TimeWidgetsMixin
|
||||
from streamlit.elements.write import WriteMixin
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto import Block_pb2, ForwardMsg_pb2
|
||||
from streamlit.proto.RootContainer_pb2 import RootContainer
|
||||
from streamlit.runtime import caching
|
||||
from streamlit.runtime.scriptrunner import enqueue_message as _enqueue_message
|
||||
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from google.protobuf.message import Message
|
||||
|
||||
from streamlit.cursor import Cursor
|
||||
from streamlit.elements.lib.built_in_chart_utils import AddRowsMetadata
|
||||
|
||||
MAX_DELTA_BYTES: Final[int] = 14 * 1024 * 1024 # 14MB
|
||||
|
||||
Value = TypeVar("Value")
|
||||
|
||||
# Type aliases for Ancestor Block Types
|
||||
BlockType: TypeAlias = str
|
||||
AncestorBlockTypes: TypeAlias = Iterable[BlockType]
|
||||
|
||||
|
||||
_use_warning_has_been_displayed: bool = False
|
||||
|
||||
|
||||
def _maybe_print_use_warning() -> None:
|
||||
"""Print a warning if Streamlit is imported but not being run with `streamlit run`.
|
||||
The warning is printed only once, and is printed using the root logger.
|
||||
"""
|
||||
global _use_warning_has_been_displayed
|
||||
|
||||
if not _use_warning_has_been_displayed:
|
||||
_use_warning_has_been_displayed = True
|
||||
|
||||
warning = cli_util.style_for_cli("Warning:", bold=True, fg="yellow")
|
||||
|
||||
if env_util.is_repl():
|
||||
logger.get_logger("root").warning(
|
||||
f"\n {warning} to view a Streamlit app on a browser, use Streamlit in "
|
||||
"a file and\n run it with the following command:\n\n streamlit run "
|
||||
"[FILE_NAME] [ARGUMENTS]"
|
||||
)
|
||||
|
||||
elif not runtime.exists() and config.get_option(
|
||||
"global.showWarningOnDirectExecution"
|
||||
):
|
||||
script_name = sys.argv[0]
|
||||
|
||||
logger.get_logger("root").warning(
|
||||
f"\n {warning} to view this Streamlit app on a browser, run it with "
|
||||
f"the following\n command:\n\n streamlit run {script_name} "
|
||||
"[ARGUMENTS]"
|
||||
)
|
||||
|
||||
|
||||
class DeltaGenerator(
|
||||
AlertMixin,
|
||||
AudioInputMixin,
|
||||
BalloonsMixin,
|
||||
BokehMixin,
|
||||
ButtonMixin,
|
||||
ButtonGroupMixin,
|
||||
CameraInputMixin,
|
||||
ChatMixin,
|
||||
CheckboxMixin,
|
||||
CodeMixin,
|
||||
ColorPickerMixin,
|
||||
EmptyMixin,
|
||||
ExceptionMixin,
|
||||
FileUploaderMixin,
|
||||
FormMixin,
|
||||
GraphvizMixin,
|
||||
HeadingMixin,
|
||||
HelpMixin,
|
||||
HtmlMixin,
|
||||
IframeMixin,
|
||||
ImageMixin,
|
||||
LayoutsMixin,
|
||||
MarkdownMixin,
|
||||
MapMixin,
|
||||
MediaMixin,
|
||||
MetricMixin,
|
||||
MultiSelectMixin,
|
||||
NumberInputMixin,
|
||||
PlotlyMixin,
|
||||
ProgressMixin,
|
||||
PydeckMixin,
|
||||
PyplotMixin,
|
||||
RadioMixin,
|
||||
SelectboxMixin,
|
||||
SelectSliderMixin,
|
||||
SliderMixin,
|
||||
SnowMixin,
|
||||
JsonMixin,
|
||||
TextMixin,
|
||||
TextWidgetsMixin,
|
||||
TimeWidgetsMixin,
|
||||
ToastMixin,
|
||||
WriteMixin,
|
||||
ArrowMixin,
|
||||
VegaChartsMixin,
|
||||
DataEditorMixin,
|
||||
):
|
||||
"""Creator of Delta protobuf messages.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
root_container: BlockPath_pb2.BlockPath.ContainerValue or None
|
||||
The root container for this DeltaGenerator. If None, this is a null
|
||||
DeltaGenerator which doesn't print to the app at all (useful for
|
||||
testing).
|
||||
|
||||
cursor: cursor.Cursor or None
|
||||
This is either:
|
||||
- None: if this is the running DeltaGenerator for a top-level
|
||||
container (MAIN or SIDEBAR)
|
||||
- RunningCursor: if this is the running DeltaGenerator for a
|
||||
non-top-level container (created with dg.container())
|
||||
- LockedCursor: if this is a locked DeltaGenerator returned by some
|
||||
other DeltaGenerator method. E.g. the dg returned in dg =
|
||||
st.text("foo").
|
||||
|
||||
parent: DeltaGenerator
|
||||
To support the `with dg` notation, DGs are arranged as a tree. Each DG
|
||||
remembers its own parent, and the root of the tree is the main DG.
|
||||
|
||||
block_type: None or "vertical" or "horizontal" or "column" or "expandable"
|
||||
If this is a block DG, we track its type to prevent nested columns/expanders
|
||||
|
||||
"""
|
||||
|
||||
# The pydoc below is for user consumption, so it doesn't talk about
|
||||
# DeltaGenerator constructor parameters (which users should never use). For
|
||||
# those, see above.
|
||||
def __init__(
|
||||
self,
|
||||
root_container: int | None = RootContainer.MAIN,
|
||||
cursor: Cursor | None = None,
|
||||
parent: DeltaGenerator | None = None,
|
||||
block_type: str | None = None,
|
||||
) -> None:
|
||||
"""Inserts or updates elements in Streamlit apps.
|
||||
|
||||
As a user, you should never initialize this object by hand. Instead,
|
||||
DeltaGenerator objects are initialized for you in two places:
|
||||
|
||||
1) When you call `dg = st.foo()` for some method "foo", sometimes `dg`
|
||||
is a DeltaGenerator object. You can call methods on the `dg` object to
|
||||
update the element `foo` that appears in the Streamlit app.
|
||||
|
||||
2) This is an internal detail, but `st.sidebar` itself is a
|
||||
DeltaGenerator. That's why you can call `st.sidebar.foo()` to place
|
||||
an element `foo` inside the sidebar.
|
||||
|
||||
"""
|
||||
# Sanity check our Container + Cursor, to ensure that our Cursor
|
||||
# is using the same Container that we are.
|
||||
if (
|
||||
root_container is not None
|
||||
and cursor is not None
|
||||
and root_container != cursor.root_container
|
||||
):
|
||||
raise RuntimeError(
|
||||
"DeltaGenerator root_container and cursor.root_container must be the same"
|
||||
)
|
||||
|
||||
# Whether this DeltaGenerator is nested in the main area or sidebar.
|
||||
# No relation to `st.container()`.
|
||||
self._root_container = root_container
|
||||
|
||||
# NOTE: You should never use this directly! Instead, use self._cursor,
|
||||
# which is a computed property that fetches the right cursor.
|
||||
self._provided_cursor = cursor
|
||||
|
||||
self._parent = parent
|
||||
self._block_type = block_type
|
||||
|
||||
# If this an `st.form` block, this will get filled in.
|
||||
self._form_data: FormData | None = None
|
||||
|
||||
# Change the module of all mixin'ed functions to be st.delta_generator,
|
||||
# instead of the original module (e.g. st.elements.markdown)
|
||||
for mixin in self.__class__.__bases__:
|
||||
for _, func in mixin.__dict__.items():
|
||||
if callable(func):
|
||||
func.__module__ = self.__module__
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return util.repr_(self)
|
||||
|
||||
def __enter__(self) -> None:
|
||||
# with block started
|
||||
context_dg_stack.set(context_dg_stack.get() + (self,))
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
type: Any,
|
||||
value: Any,
|
||||
traceback: Any,
|
||||
) -> Literal[False]:
|
||||
# with block ended
|
||||
|
||||
context_dg_stack.set(context_dg_stack.get()[:-1])
|
||||
|
||||
# Re-raise any exceptions
|
||||
return False
|
||||
|
||||
@property
|
||||
def _active_dg(self) -> DeltaGenerator:
|
||||
"""Return the DeltaGenerator that's currently 'active'.
|
||||
If we are the main DeltaGenerator, and are inside a `with` block that
|
||||
creates a container, our active_dg is that container. Otherwise,
|
||||
our active_dg is self.
|
||||
"""
|
||||
if self == self._main_dg:
|
||||
# We're being invoked via an `st.foo` pattern - use the current
|
||||
# `with` dg (aka the top of the stack).
|
||||
last_context_stack_dg = get_last_dg_added_to_context_stack()
|
||||
if last_context_stack_dg is not None:
|
||||
return last_context_stack_dg
|
||||
|
||||
# We're being invoked via an `st.sidebar.foo` pattern - ignore the
|
||||
# current `with` dg.
|
||||
return self
|
||||
|
||||
@property
|
||||
def _main_dg(self) -> DeltaGenerator:
|
||||
"""Return this DeltaGenerator's root - that is, the top-level ancestor
|
||||
DeltaGenerator that we belong to (this generally means the st._main
|
||||
DeltaGenerator).
|
||||
"""
|
||||
return self._parent._main_dg if self._parent else self
|
||||
|
||||
def __getattr__(self, name: str) -> Callable[..., NoReturn]:
|
||||
import streamlit as st
|
||||
|
||||
streamlit_methods = [
|
||||
method_name for method_name in dir(st) if callable(getattr(st, method_name))
|
||||
]
|
||||
|
||||
def wrapper(*args: Any, **kwargs: Any) -> NoReturn:
|
||||
if name in streamlit_methods:
|
||||
if self._root_container == RootContainer.SIDEBAR:
|
||||
message = (
|
||||
f"Method `{name}()` does not exist for "
|
||||
f"`st.sidebar`. Did you mean `st.{name}()`?"
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
f"Method `{name}()` does not exist for "
|
||||
"`DeltaGenerator` objects. Did you mean "
|
||||
f"`st.{name}()`?"
|
||||
)
|
||||
else:
|
||||
message = f"`{name}()` is not a valid Streamlit command."
|
||||
|
||||
raise StreamlitAPIException(message)
|
||||
|
||||
return wrapper
|
||||
|
||||
def __deepcopy__(self, _memo):
|
||||
dg = DeltaGenerator(
|
||||
root_container=self._root_container,
|
||||
cursor=deepcopy(self._cursor),
|
||||
parent=deepcopy(self._parent),
|
||||
block_type=self._block_type,
|
||||
)
|
||||
dg._form_data = deepcopy(self._form_data)
|
||||
return dg
|
||||
|
||||
@property
|
||||
def _ancestors(self) -> Iterable[DeltaGenerator]:
|
||||
current_dg: DeltaGenerator | None = self
|
||||
while current_dg is not None:
|
||||
yield current_dg
|
||||
current_dg = current_dg._parent
|
||||
|
||||
@property
|
||||
def _ancestor_block_types(self) -> AncestorBlockTypes:
|
||||
"""Iterate all the block types used by this DeltaGenerator and all
|
||||
its ancestor DeltaGenerators.
|
||||
"""
|
||||
for a in self._ancestors:
|
||||
if a._block_type is not None:
|
||||
yield a._block_type
|
||||
|
||||
def _count_num_of_parent_columns(
|
||||
self, ancestor_block_types: AncestorBlockTypes
|
||||
) -> int:
|
||||
return sum(
|
||||
1 for ancestor_block in ancestor_block_types if ancestor_block == "column"
|
||||
)
|
||||
|
||||
@property
|
||||
def _cursor(self) -> Cursor | None:
|
||||
"""Return our Cursor. This will be None if we're not running in a
|
||||
ScriptThread - e.g., if we're running a "bare" script outside of
|
||||
Streamlit.
|
||||
"""
|
||||
if self._provided_cursor is None:
|
||||
return cursor.get_container_cursor(self._root_container)
|
||||
else:
|
||||
return self._provided_cursor
|
||||
|
||||
@property
|
||||
def _is_top_level(self) -> bool:
|
||||
return self._provided_cursor is None
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
return str(id(self))
|
||||
|
||||
def _get_delta_path_str(self) -> str:
|
||||
"""Returns the element's delta path as a string like "[0, 2, 3, 1]".
|
||||
|
||||
This uniquely identifies the element's position in the front-end,
|
||||
which allows (among other potential uses) the MediaFileManager to maintain
|
||||
session-specific maps of MediaFile objects placed with their "coordinates".
|
||||
|
||||
This way, users can (say) use st.image with a stream of different images,
|
||||
and Streamlit will expire the older images and replace them in place.
|
||||
"""
|
||||
# Operate on the active DeltaGenerator, in case we're in a `with` block.
|
||||
dg = self._active_dg
|
||||
return str(dg._cursor.delta_path) if dg._cursor is not None else "[]"
|
||||
|
||||
def _enqueue(
|
||||
self,
|
||||
delta_type: str,
|
||||
element_proto: Message,
|
||||
add_rows_metadata: AddRowsMetadata | None = None,
|
||||
user_key: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Create NewElement delta, fill it, and enqueue it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
delta_type : str
|
||||
The name of the streamlit method being called
|
||||
element_proto : proto
|
||||
The actual proto in the NewElement type e.g. Alert/Button/Slider
|
||||
add_rows_metadata : AddRowsMetadata or None
|
||||
Metadata for the add_rows method
|
||||
user_key : str or None
|
||||
A custom key for the element provided by the user.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DeltaGenerator
|
||||
Return a DeltaGenerator that can be used to modify the newly-created
|
||||
element.
|
||||
"""
|
||||
# Operate on the active DeltaGenerator, in case we're in a `with` block.
|
||||
dg = self._active_dg
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx and ctx.current_fragment_id and _writes_directly_to_sidebar(dg):
|
||||
raise StreamlitAPIException(
|
||||
"Calling `st.sidebar` in a function wrapped with `st.fragment` is not "
|
||||
"supported. To write elements to the sidebar with a fragment, call your "
|
||||
"fragment function inside a `with st.sidebar` context manager."
|
||||
)
|
||||
|
||||
# Warn if an element is being changed but the user isn't running the streamlit server.
|
||||
_maybe_print_use_warning()
|
||||
|
||||
# Copy the marshalled proto into the overall msg proto
|
||||
msg = ForwardMsg_pb2.ForwardMsg()
|
||||
msg_el_proto = getattr(msg.delta.new_element, delta_type)
|
||||
msg_el_proto.CopyFrom(element_proto)
|
||||
|
||||
# Only enqueue message and fill in metadata if there's a container.
|
||||
msg_was_enqueued = False
|
||||
if dg._root_container is not None and dg._cursor is not None:
|
||||
msg.metadata.delta_path[:] = dg._cursor.delta_path
|
||||
|
||||
_enqueue_message(msg)
|
||||
msg_was_enqueued = True
|
||||
|
||||
if msg_was_enqueued:
|
||||
# Get a DeltaGenerator that is locked to the current element
|
||||
# position.
|
||||
new_cursor = (
|
||||
dg._cursor.get_locked_cursor(
|
||||
delta_type=delta_type, add_rows_metadata=add_rows_metadata
|
||||
)
|
||||
if dg._cursor is not None
|
||||
else None
|
||||
)
|
||||
|
||||
output_dg = DeltaGenerator(
|
||||
root_container=dg._root_container,
|
||||
cursor=new_cursor,
|
||||
parent=dg,
|
||||
)
|
||||
else:
|
||||
# If the message was not enqueued, just return self since it's a
|
||||
# no-op from the point of view of the app.
|
||||
output_dg = dg
|
||||
|
||||
# Save message for replay if we're called from within @st.cache_data or @st.cache_resource
|
||||
caching.save_element_message(
|
||||
delta_type,
|
||||
element_proto,
|
||||
invoked_dg_id=self.id,
|
||||
used_dg_id=dg.id,
|
||||
returned_dg_id=output_dg.id,
|
||||
)
|
||||
|
||||
return output_dg
|
||||
|
||||
def _block(
|
||||
self,
|
||||
block_proto: Block_pb2.Block = Block_pb2.Block(),
|
||||
dg_type: type | None = None,
|
||||
) -> DeltaGenerator:
|
||||
# Operate on the active DeltaGenerator, in case we're in a `with` block.
|
||||
dg = self._active_dg
|
||||
|
||||
# Prevent nested columns & expanders by checking all parents.
|
||||
block_type = block_proto.WhichOneof("type")
|
||||
# Convert the generator to a list, so we can use it multiple times.
|
||||
ancestor_block_types = list(dg._ancestor_block_types)
|
||||
_check_nested_element_violation(self, block_type, ancestor_block_types)
|
||||
|
||||
if dg._root_container is None or dg._cursor is None:
|
||||
return dg
|
||||
|
||||
msg = ForwardMsg_pb2.ForwardMsg()
|
||||
msg.metadata.delta_path[:] = dg._cursor.delta_path
|
||||
msg.delta.add_block.CopyFrom(block_proto)
|
||||
|
||||
# Normally we'd return a new DeltaGenerator that uses the locked cursor
|
||||
# below. But in this case we want to return a DeltaGenerator that uses
|
||||
# a brand new cursor for this new block we're creating.
|
||||
block_cursor = cursor.RunningCursor(
|
||||
root_container=dg._root_container,
|
||||
parent_path=dg._cursor.parent_path + (dg._cursor.index,),
|
||||
)
|
||||
|
||||
# `dg_type` param added for st.status container. It allows us to
|
||||
# instantiate DeltaGenerator subclasses from the function.
|
||||
if dg_type is None:
|
||||
dg_type = DeltaGenerator
|
||||
|
||||
block_dg = cast(
|
||||
"DeltaGenerator",
|
||||
dg_type(
|
||||
root_container=dg._root_container,
|
||||
cursor=block_cursor,
|
||||
parent=dg,
|
||||
block_type=block_type,
|
||||
),
|
||||
)
|
||||
# Blocks inherit their parent form ids.
|
||||
# NOTE: Container form ids aren't set in proto.
|
||||
block_dg._form_data = FormData(current_form_id(dg))
|
||||
|
||||
# Must be called to increment this cursor's index.
|
||||
dg._cursor.get_locked_cursor(add_rows_metadata=None)
|
||||
_enqueue_message(msg)
|
||||
|
||||
caching.save_block_message(
|
||||
block_proto,
|
||||
invoked_dg_id=self.id,
|
||||
used_dg_id=dg.id,
|
||||
returned_dg_id=block_dg.id,
|
||||
)
|
||||
|
||||
return block_dg
|
||||
|
||||
|
||||
def _writes_directly_to_sidebar(dg: DeltaGenerator) -> bool:
|
||||
in_sidebar = any(a._root_container == RootContainer.SIDEBAR for a in dg._ancestors)
|
||||
has_container = bool(list(dg._ancestor_block_types))
|
||||
return in_sidebar and not has_container
|
||||
|
||||
|
||||
def _check_nested_element_violation(
|
||||
dg: DeltaGenerator, block_type: str | None, ancestor_block_types: list[BlockType]
|
||||
) -> None:
|
||||
"""Check if elements are nested in a forbidden way.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException: throw if an invalid element nesting is detected.
|
||||
"""
|
||||
|
||||
if block_type == "column":
|
||||
num_of_parent_columns = dg._count_num_of_parent_columns(ancestor_block_types)
|
||||
if dg._root_container == RootContainer.SIDEBAR and num_of_parent_columns > 0:
|
||||
raise StreamlitAPIException(
|
||||
"Columns cannot be placed inside other columns in the sidebar. This is only possible in the main area of the app."
|
||||
)
|
||||
if num_of_parent_columns > 1:
|
||||
raise StreamlitAPIException(
|
||||
"Columns can only be placed inside other columns up to one level of nesting."
|
||||
)
|
||||
if block_type == "chat_message" and block_type in ancestor_block_types:
|
||||
raise StreamlitAPIException(
|
||||
"Chat messages cannot nested inside other chat messages."
|
||||
)
|
||||
if block_type == "expandable" and block_type in ancestor_block_types:
|
||||
raise StreamlitAPIException(
|
||||
"Expanders may not be nested inside other expanders."
|
||||
)
|
||||
if block_type == "popover" and block_type in ancestor_block_types:
|
||||
raise StreamlitAPIException("Popovers may not be nested inside other popovers.")
|
||||
@@ -0,0 +1,204 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""
|
||||
The main purpose of this module (right now at least) is to avoid a dependency
|
||||
cycle between streamlit.delta_generator and some elements.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextvars import ContextVar, Token
|
||||
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
|
||||
|
||||
from streamlit.proto.RootContainer_pb2 import RootContainer as _RootContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.elements.lib.dialog import Dialog
|
||||
from streamlit.elements.lib.mutable_status_container import StatusContainer
|
||||
|
||||
|
||||
class DeltaGeneratorSingleton:
|
||||
"""Used to initialize the DeltaGenerator classes and store them as singletons.
|
||||
This module allows us to avoid circular imports between DeltaGenerator and elements,
|
||||
because elemens can import this singleton module instead of DeltaGenerator directly.
|
||||
"""
|
||||
|
||||
_instance: DeltaGeneratorSingleton | None = None
|
||||
|
||||
@classmethod
|
||||
def instance(cls) -> DeltaGeneratorSingleton:
|
||||
"""Return the singleton DeltaGeneratorSingleton instance. Raise an Error if the
|
||||
DeltaGeneratorSingleton hasn't been created yet.
|
||||
"""
|
||||
if cls._instance is None:
|
||||
raise RuntimeError("DeltaGeneratorSingleton hasn't been created!")
|
||||
return cls._instance
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
delta_generator_cls: type[DeltaGenerator],
|
||||
status_container_cls: type[StatusContainer],
|
||||
dialog_container_cls: type[Dialog],
|
||||
):
|
||||
"""Registers and initializes all delta-generator classes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
delta_generator_cls : type[DeltaGenerator]
|
||||
The main DeltaGenerator class.
|
||||
status_container_cls : type[StatusContainer]
|
||||
The delta-generator class that is used as return value for `st.status`.
|
||||
dialog_container_cls : type[Dialog]
|
||||
The delta-generator class used is used as return value for `st.dialog`.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the DeltaGeneratorSingleton instance already exists.
|
||||
"""
|
||||
if DeltaGeneratorSingleton._instance is not None:
|
||||
raise RuntimeError("DeltaGeneratorSingleton instance already exists!")
|
||||
DeltaGeneratorSingleton._instance = self
|
||||
|
||||
self._main_dg = delta_generator_cls(root_container=_RootContainer.MAIN)
|
||||
self._sidebar_dg = delta_generator_cls(
|
||||
root_container=_RootContainer.SIDEBAR, parent=self._main_dg
|
||||
)
|
||||
self._event_dg = delta_generator_cls(
|
||||
root_container=_RootContainer.EVENT, parent=self._main_dg
|
||||
)
|
||||
self._bottom_dg = delta_generator_cls(
|
||||
root_container=_RootContainer.BOTTOM, parent=self._main_dg
|
||||
)
|
||||
self._status_container_cls = status_container_cls
|
||||
self._dialog_container_cls = dialog_container_cls
|
||||
|
||||
@property
|
||||
def main_dg(self) -> DeltaGenerator:
|
||||
return self._main_dg
|
||||
|
||||
@property
|
||||
def sidebar_dg(self) -> DeltaGenerator:
|
||||
return self._sidebar_dg
|
||||
|
||||
@property
|
||||
def event_dg(self) -> DeltaGenerator:
|
||||
return self._event_dg
|
||||
|
||||
@property
|
||||
def bottom_dg(self) -> DeltaGenerator:
|
||||
return self._bottom_dg
|
||||
|
||||
@property
|
||||
def status_container_cls(
|
||||
self,
|
||||
) -> type[StatusContainer]:
|
||||
"""Stub for StatusContainer. Since StatusContainer inherits from DeltaGenerator,
|
||||
this is used to avoid circular imports.
|
||||
"""
|
||||
return self._status_container_cls
|
||||
|
||||
@property
|
||||
def dialog_container_cls(self) -> type[Dialog]:
|
||||
"""Stub for Dialog. Since Dialog inherits from DeltaGenerator,
|
||||
this is used to avoid circular imports.
|
||||
"""
|
||||
return self._dialog_container_cls
|
||||
|
||||
|
||||
def get_dg_singleton_instance() -> DeltaGeneratorSingleton:
|
||||
"""Return the DeltaGeneratorSingleton instance. Raise an Error if the
|
||||
DeltaGeneratorSingleton hasn't been created yet.
|
||||
"""
|
||||
return DeltaGeneratorSingleton.instance()
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class ContextVarWithLazyDefault(Generic[_T]):
|
||||
"""The dg_stack tracks the currently active DeltaGenerator, and is pushed to when
|
||||
a DeltaGenerator is entered via a `with` block. This is implemented as a ContextVar
|
||||
so that different threads or async tasks can have their own stacks.
|
||||
|
||||
We have a wrapper around it because ContextVar default cannot be a function, but
|
||||
the default dg (main_dg) might not exist yet when this module is imported.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, *, default: Callable[[], _T]):
|
||||
self._name = name
|
||||
self._default = default
|
||||
self._context_var: ContextVar[_T] | None = None
|
||||
|
||||
def _init_context_var(self) -> None:
|
||||
self._context_var = ContextVar(self._name, default=self._default()) # noqa: B039
|
||||
|
||||
def get(self) -> _T:
|
||||
if self._context_var is None:
|
||||
self._init_context_var()
|
||||
return self._context_var.get() # type: ignore[union-attr]
|
||||
|
||||
def set(self, value: _T) -> Token[_T]:
|
||||
if self._context_var is None:
|
||||
self._init_context_var()
|
||||
return self._context_var.set(value) # type: ignore[union-attr]
|
||||
|
||||
def reset(self, token: Token[_T]) -> None:
|
||||
if self._context_var is None:
|
||||
self._init_context_var()
|
||||
self._context_var.reset(token) # type: ignore[union-attr]
|
||||
|
||||
def __hash__(self) -> int:
|
||||
if self._context_var is None:
|
||||
self._init_context_var()
|
||||
return self._context_var.__hash__()
|
||||
|
||||
|
||||
# we don't use the default factory here because `main_dg` is not initialized when this
|
||||
# module is imported. This is why we have our own ContextVar wrapper.
|
||||
context_dg_stack: ContextVarWithLazyDefault[tuple[DeltaGenerator, ...]] = (
|
||||
ContextVarWithLazyDefault(
|
||||
"context_dg_stack", default=lambda: (get_dg_singleton_instance().main_dg,)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_default_dg_stack_value() -> tuple[DeltaGenerator, ...]:
|
||||
"""Get the default dg_stack value with which the dg_stack should
|
||||
be initialized and reset if needed.
|
||||
"""
|
||||
instance = get_dg_singleton_instance()
|
||||
if instance._main_dg is None:
|
||||
raise RuntimeError("main_dg is not set")
|
||||
|
||||
return (instance._main_dg,)
|
||||
|
||||
|
||||
def get_last_dg_added_to_context_stack() -> DeltaGenerator | None:
|
||||
"""Get the last added DeltaGenerator of the stack in the current context.
|
||||
|
||||
Returns None if the stack has only one element or is empty for whatever reason.
|
||||
"""
|
||||
current_stack = context_dg_stack.get()
|
||||
# If set to "> 0" and thus return the only delta generator in the stack -
|
||||
# which logically makes more sense -, some unit tests fail.
|
||||
# It looks like the reason is that they create their own main delta generator
|
||||
# but do not populate the dg_stack correctly. However, to be on the safe-side,
|
||||
# we keep the logic but leave the comment as shared knowledge for whoever will look
|
||||
# into this in the future.
|
||||
if len(current_stack) > 1:
|
||||
return current_stack[-1]
|
||||
return None
|
||||
209
myenv/lib/python3.11/site-packages/streamlit/deprecation_util.py
Normal file
209
myenv/lib/python3.11/site-packages/streamlit/deprecation_util.py
Normal file
@@ -0,0 +1,209 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any, Callable, Final, TypeVar, cast
|
||||
|
||||
import streamlit
|
||||
from streamlit import config
|
||||
from streamlit.logger import get_logger
|
||||
|
||||
_LOGGER: Final = get_logger(__name__)
|
||||
|
||||
TFunc = TypeVar("TFunc", bound=Callable[..., Any])
|
||||
TObj = TypeVar("TObj", bound=object)
|
||||
|
||||
|
||||
def _should_show_deprecation_warning_in_browser() -> bool:
|
||||
"""True if we should print deprecation warnings to the browser."""
|
||||
return bool(config.get_option("client.showErrorDetails"))
|
||||
|
||||
|
||||
def show_deprecation_warning(message: str) -> None:
|
||||
"""Show a deprecation warning message."""
|
||||
if _should_show_deprecation_warning_in_browser():
|
||||
streamlit.warning(message)
|
||||
|
||||
# We always log deprecation warnings
|
||||
_LOGGER.warning(message)
|
||||
|
||||
|
||||
def make_deprecated_name_warning(
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
removal_date: str,
|
||||
extra_message: str | None = None,
|
||||
include_st_prefix: bool = True,
|
||||
) -> str:
|
||||
if include_st_prefix:
|
||||
old_name = f"st.{old_name}"
|
||||
new_name = f"st.{new_name}"
|
||||
|
||||
return (
|
||||
f"Please replace `{old_name}` with `{new_name}`.\n\n"
|
||||
f"`{old_name}` will be removed after {removal_date}."
|
||||
+ (f"\n\n{extra_message}" if extra_message else "")
|
||||
)
|
||||
|
||||
|
||||
def deprecate_func_name(
|
||||
func: TFunc,
|
||||
old_name: str,
|
||||
removal_date: str,
|
||||
extra_message: str | None = None,
|
||||
name_override: str | None = None,
|
||||
) -> TFunc:
|
||||
"""Wrap an `st` function whose name has changed.
|
||||
|
||||
Wrapped functions will run as normal, but will also show an st.warning
|
||||
saying that the old name will be removed after removal_date.
|
||||
|
||||
(We generally set `removal_date` to 3 months from the deprecation date.)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func
|
||||
The `st.` function whose name has changed.
|
||||
|
||||
old_name
|
||||
The function's deprecated name within __init__.py.
|
||||
|
||||
removal_date
|
||||
A date like "2020-01-01", indicating the last day we'll guarantee
|
||||
support for the deprecated name.
|
||||
|
||||
extra_message
|
||||
An optional extra message to show in the deprecation warning.
|
||||
|
||||
name_override
|
||||
An optional name to use in place of func.__name__.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapped_func(*args, **kwargs):
|
||||
result = func(*args, **kwargs)
|
||||
show_deprecation_warning(
|
||||
make_deprecated_name_warning(
|
||||
old_name, name_override or func.__name__, removal_date, extra_message
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Update the wrapped func's name & docstring so st.help does the right thing
|
||||
wrapped_func.__name__ = old_name
|
||||
wrapped_func.__doc__ = func.__doc__
|
||||
return cast("TFunc", wrapped_func)
|
||||
|
||||
|
||||
def deprecate_obj_name(
|
||||
obj: TObj,
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
removal_date: str,
|
||||
include_st_prefix: bool = True,
|
||||
) -> TObj:
|
||||
"""Wrap an `st` object whose name has changed.
|
||||
|
||||
Wrapped objects will behave as normal, but will also show an st.warning
|
||||
saying that the old name will be removed after `removal_date`.
|
||||
|
||||
(We generally set `removal_date` to 3 months from the deprecation date.)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj
|
||||
The `st.` object whose name has changed.
|
||||
|
||||
old_name
|
||||
The object's deprecated name within __init__.py.
|
||||
|
||||
new_name
|
||||
The object's new name within __init__.py.
|
||||
|
||||
removal_date
|
||||
A date like "2020-01-01", indicating the last day we'll guarantee
|
||||
support for the deprecated name.
|
||||
|
||||
include_st_prefix
|
||||
If False, does not prefix each of the object names in the deprecation
|
||||
essage with `st.*`. Defaults to True.
|
||||
"""
|
||||
|
||||
return _create_deprecated_obj_wrapper(
|
||||
obj,
|
||||
lambda: show_deprecation_warning(
|
||||
make_deprecated_name_warning(
|
||||
old_name, new_name, removal_date, include_st_prefix=include_st_prefix
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _create_deprecated_obj_wrapper(obj: TObj, show_warning: Callable[[], Any]) -> TObj:
|
||||
"""Create a wrapper for an object that has been deprecated. The first
|
||||
time one of the object's properties or functions is accessed, the
|
||||
given `show_warning` callback will be called.
|
||||
"""
|
||||
has_shown_warning = False
|
||||
|
||||
def maybe_show_warning() -> None:
|
||||
# Call `show_warning` if it hasn't already been called once.
|
||||
nonlocal has_shown_warning
|
||||
if not has_shown_warning:
|
||||
has_shown_warning = True
|
||||
show_warning()
|
||||
|
||||
class Wrapper:
|
||||
def __init__(self):
|
||||
# Override all the Wrapped object's magic functions
|
||||
for name in Wrapper._get_magic_functions(obj.__class__):
|
||||
setattr(
|
||||
self.__class__,
|
||||
name,
|
||||
property(self._make_magic_function_proxy(name)),
|
||||
)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# We handle __getattr__ separately from our other magic
|
||||
# functions. The wrapped class may not actually implement it,
|
||||
# but we still need to implement it to call all its normal
|
||||
# functions.
|
||||
if attr in self.__dict__:
|
||||
return getattr(self, attr)
|
||||
|
||||
maybe_show_warning()
|
||||
return getattr(obj, attr)
|
||||
|
||||
@staticmethod
|
||||
def _get_magic_functions(cls) -> list[str]:
|
||||
# ignore the handful of magic functions we cannot override without
|
||||
# breaking the Wrapper.
|
||||
ignore = ("__class__", "__dict__", "__getattribute__", "__getattr__")
|
||||
return [
|
||||
name
|
||||
for name in dir(cls)
|
||||
if name not in ignore and name.startswith("__")
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _make_magic_function_proxy(name):
|
||||
def proxy(self, *args):
|
||||
maybe_show_warning()
|
||||
return getattr(obj, name)
|
||||
|
||||
return proxy
|
||||
|
||||
return cast("TObj", Wrapper())
|
||||
21
myenv/lib/python3.11/site-packages/streamlit/development.py
Normal file
21
myenv/lib/python3.11/site-packages/streamlit/development.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Variables for dev purposes.
|
||||
|
||||
The main purpose of this module (right now at least) is to avoid a dependency
|
||||
cycle between streamlit.config and streamlit.logger.
|
||||
"""
|
||||
|
||||
is_development_mode = False
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
234
myenv/lib/python3.11/site-packages/streamlit/elements/alert.py
Normal file
234
myenv/lib/python3.11/site-packages/streamlit/elements/alert.py
Normal file
@@ -0,0 +1,234 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Alert_pb2 import Alert as AlertProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text, validate_icon_or_emoji
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class AlertMixin:
|
||||
@gather_metrics("error")
|
||||
def error(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display error message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.error('This is an error', icon="🚨")
|
||||
|
||||
"""
|
||||
alert_proto = AlertProto()
|
||||
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.format = AlertProto.ERROR
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@gather_metrics("warning")
|
||||
def warning(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display warning message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.warning('This is a warning', icon="⚠️")
|
||||
|
||||
"""
|
||||
alert_proto = AlertProto()
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.format = AlertProto.WARNING
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@gather_metrics("info")
|
||||
def info(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an informational message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.info('This is a purely informational message', icon="ℹ️")
|
||||
|
||||
"""
|
||||
|
||||
alert_proto = AlertProto()
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.format = AlertProto.INFO
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@gather_metrics("success")
|
||||
def success(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a success message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.success('This is a success message!', icon="✅")
|
||||
|
||||
"""
|
||||
alert_proto = AlertProto()
|
||||
alert_proto.body = clean_text(body)
|
||||
alert_proto.icon = validate_icon_or_emoji(icon)
|
||||
alert_proto.format = AlertProto.SUCCESS
|
||||
return self.dg._enqueue("alert", alert_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
961
myenv/lib/python3.11/site-packages/streamlit/elements/arrow.py
Normal file
961
myenv/lib/python3.11/site-packages/streamlit/elements/arrow.py
Normal file
@@ -0,0 +1,961 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import dataframe_util
|
||||
from streamlit.elements.lib.column_config_utils import (
|
||||
INDEX_IDENTIFIER,
|
||||
ColumnConfigMappingInput,
|
||||
apply_data_specific_configs,
|
||||
marshall_column_config,
|
||||
process_config_mapping,
|
||||
update_column_config,
|
||||
)
|
||||
from streamlit.elements.lib.event_utils import AttributeDictionary
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.pandas_styler_utils import marshall_styler
|
||||
from streamlit.elements.lib.policies import check_widget_policies
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
enqueue_message,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
from streamlit.runtime.state import WidgetCallback, register_widget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Hashable, Iterable
|
||||
|
||||
from numpy import typing as npt
|
||||
from pandas import DataFrame
|
||||
|
||||
from streamlit.dataframe_util import Data
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.elements.lib.built_in_chart_utils import AddRowsMetadata
|
||||
|
||||
|
||||
SelectionMode: TypeAlias = Literal[
|
||||
"single-row", "multi-row", "single-column", "multi-column"
|
||||
]
|
||||
_SELECTION_MODES: Final[set[SelectionMode]] = {
|
||||
"single-row",
|
||||
"multi-row",
|
||||
"single-column",
|
||||
"multi-column",
|
||||
}
|
||||
|
||||
|
||||
class DataframeSelectionState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the dataframe selection state.
|
||||
|
||||
The selection state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Selection states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
.. warning::
|
||||
If a user sorts a dataframe, row selections will be reset. If your
|
||||
users need to sort and filter the dataframe to make selections, direct
|
||||
them to use the search function in the dataframe toolbar instead.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
rows : list[int]
|
||||
The selected rows, identified by their integer position. The integer
|
||||
positions match the original dataframe, even if the user sorts the
|
||||
dataframe in their browser. For a ``pandas.DataFrame``, you can
|
||||
retrieve data from its interger position using methods like ``.iloc[]``
|
||||
or ``.iat[]``.
|
||||
columns : list[str]
|
||||
The selected columns, identified by their names.
|
||||
|
||||
Example
|
||||
-------
|
||||
The following example has multi-row and multi-column selections enabled.
|
||||
Try selecting some rows. To select multiple columns, hold ``Ctrl`` while
|
||||
selecting columns. Hold ``Shift`` to select a range of columns.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> if "df" not in st.session_state:
|
||||
>>> st.session_state.df = pd.DataFrame(
|
||||
... np.random.randn(12, 5), columns=["a", "b", "c", "d", "e"]
|
||||
... )
|
||||
>>>
|
||||
>>> event = st.dataframe(
|
||||
... st.session_state.df,
|
||||
... key="data",
|
||||
... on_select="rerun",
|
||||
... selection_mode=["multi-row", "multi-column"],
|
||||
... )
|
||||
>>>
|
||||
>>> event.selection
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe-events-selection-state.streamlit.app
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
|
||||
rows: list[int]
|
||||
columns: list[str]
|
||||
|
||||
|
||||
class DataframeState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the dataframe event state.
|
||||
|
||||
The event state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Event states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
Only selection events are supported at this time.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
selection : dict
|
||||
The state of the ``on_select`` event. This attribute returns a
|
||||
dictionary-like object that supports both key and attribute notation.
|
||||
The attributes are described by the ``DataframeSelectionState``
|
||||
dictionary schema.
|
||||
|
||||
|
||||
"""
|
||||
|
||||
selection: DataframeSelectionState
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataframeSelectionSerde:
|
||||
"""DataframeSelectionSerde is used to serialize and deserialize the dataframe selection state."""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> DataframeState:
|
||||
empty_selection_state: DataframeState = {
|
||||
"selection": {
|
||||
"rows": [],
|
||||
"columns": [],
|
||||
},
|
||||
}
|
||||
selection_state: DataframeState = (
|
||||
empty_selection_state if ui_value is None else json.loads(ui_value)
|
||||
)
|
||||
|
||||
if "selection" not in selection_state:
|
||||
selection_state = empty_selection_state
|
||||
|
||||
return cast("DataframeState", AttributeDictionary(selection_state))
|
||||
|
||||
def serialize(self, editing_state: DataframeState) -> str:
|
||||
return json.dumps(editing_state, default=str)
|
||||
|
||||
|
||||
def parse_selection_mode(
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode],
|
||||
) -> set[ArrowProto.SelectionMode.ValueType]:
|
||||
"""Parse and check the user provided selection modes."""
|
||||
if isinstance(selection_mode, str):
|
||||
# Only a single selection mode was passed
|
||||
selection_mode_set = {selection_mode}
|
||||
else:
|
||||
# Multiple selection modes were passed
|
||||
selection_mode_set = set(selection_mode)
|
||||
|
||||
if not selection_mode_set.issubset(_SELECTION_MODES):
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. "
|
||||
f"Valid options are: {_SELECTION_MODES}"
|
||||
)
|
||||
|
||||
if selection_mode_set.issuperset({"single-row", "multi-row"}):
|
||||
raise StreamlitAPIException(
|
||||
"Only one of `single-row` or `multi-row` can be selected as selection mode."
|
||||
)
|
||||
|
||||
if selection_mode_set.issuperset({"single-column", "multi-column"}):
|
||||
raise StreamlitAPIException(
|
||||
"Only one of `single-column` or `multi-column` can be selected as selection mode."
|
||||
)
|
||||
|
||||
parsed_selection_modes = []
|
||||
for selection_mode in selection_mode_set:
|
||||
if selection_mode == "single-row":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.SINGLE_ROW)
|
||||
elif selection_mode == "multi-row":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.MULTI_ROW)
|
||||
elif selection_mode == "single-column":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.SINGLE_COLUMN)
|
||||
elif selection_mode == "multi-column":
|
||||
parsed_selection_modes.append(ArrowProto.SelectionMode.MULTI_COLUMN)
|
||||
return set(parsed_selection_modes)
|
||||
|
||||
|
||||
class ArrowMixin:
|
||||
@overload
|
||||
def dataframe(
|
||||
self,
|
||||
data: Data = None,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
*,
|
||||
use_container_width: bool | None = None,
|
||||
hide_index: bool | None = None,
|
||||
column_order: Iterable[str] | None = None,
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
key: Key | None = None,
|
||||
on_select: Literal["ignore"] = "ignore",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = "multi-row",
|
||||
row_height: int | None = None,
|
||||
) -> DeltaGenerator: ...
|
||||
|
||||
@overload
|
||||
def dataframe(
|
||||
self,
|
||||
data: Data = None,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
*,
|
||||
use_container_width: bool | None = None,
|
||||
hide_index: bool | None = None,
|
||||
column_order: Iterable[str] | None = None,
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
key: Key | None = None,
|
||||
on_select: Literal["rerun"] | WidgetCallback,
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = "multi-row",
|
||||
row_height: int | None = None,
|
||||
) -> DataframeState: ...
|
||||
|
||||
@gather_metrics("dataframe")
|
||||
def dataframe(
|
||||
self,
|
||||
data: Data = None,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
*,
|
||||
use_container_width: bool | None = None,
|
||||
hide_index: bool | None = None,
|
||||
column_order: Iterable[str] | None = None,
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
key: Key | None = None,
|
||||
on_select: Literal["ignore", "rerun"] | WidgetCallback = "ignore",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = "multi-row",
|
||||
row_height: int | None = None,
|
||||
) -> DeltaGenerator | DataframeState:
|
||||
"""Display a dataframe as an interactive table.
|
||||
|
||||
This command works with a wide variety of collection-like and
|
||||
dataframe-like object types.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : dataframe-like, collection-like, or None
|
||||
The data to display.
|
||||
|
||||
Dataframe-like objects include dataframe and series objects from
|
||||
popular libraries like Dask, Modin, Numpy, pandas, Polars, PyArrow,
|
||||
Snowpark, Xarray, and more. You can use database cursors and
|
||||
clients that comply with the
|
||||
`Python Database API Specification v2.0 (PEP 249)
|
||||
<https://peps.python.org/pep-0249/>`_. Additionally, you can use
|
||||
anything that supports the `Python dataframe interchange protocol
|
||||
<https://data-apis.org/dataframe-protocol/latest/>`_.
|
||||
|
||||
For example, you can use the following:
|
||||
|
||||
- ``pandas.DataFrame``, ``pandas.Series``, ``pandas.Index``,
|
||||
``pandas.Styler``, and ``pandas.Array``
|
||||
- ``polars.DataFrame``, ``polars.LazyFrame``, and ``polars.Series``
|
||||
- ``snowflake.snowpark.dataframe.DataFrame``,
|
||||
``snowflake.snowpark.table.Table``
|
||||
|
||||
If a data type is not recognized, Streamlit will convert the object
|
||||
to a ``pandas.DataFrame`` or ``pyarrow.Table`` using a
|
||||
``.to_pandas()`` or ``.to_arrow()`` method, respectively, if
|
||||
available.
|
||||
|
||||
If ``data`` is a ``pandas.Styler``, it will be used to style its
|
||||
underlying ``pandas.DataFrame``. Streamlit supports custom cell
|
||||
values and colors. It does not support some of the more exotic
|
||||
styling options, like bar charts, hovering, and captions. For
|
||||
these styling options, use column configuration instead. Text and
|
||||
number formatting from ``column_config`` always takes precedence
|
||||
over text and number formatting from ``pandas.Styler``.
|
||||
|
||||
Collection-like objects include all Python-native ``Collection``
|
||||
types, such as ``dict``, ``list``, and ``set``.
|
||||
|
||||
If ``data`` is ``None``, Streamlit renders an empty table.
|
||||
|
||||
width : int or None
|
||||
Desired width of the dataframe expressed in pixels. If ``width`` is
|
||||
``None`` (default), Streamlit sets the dataframe width to fit its
|
||||
contents up to the width of the parent container. If ``width`` is
|
||||
greater than the width of the parent container, Streamlit sets the
|
||||
dataframe width to match the width of the parent container.
|
||||
|
||||
height : int or None
|
||||
Desired height of the dataframe expressed in pixels. If ``height``
|
||||
is ``None`` (default), Streamlit sets the height to show at most
|
||||
ten rows. Vertical scrolling within the dataframe element is
|
||||
enabled when the height does not accomodate all rows.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override ``width`` with the width of the parent
|
||||
container. If this is ``True`` (default), Streamlit sets the width
|
||||
of the dataframe to match the width of the parent container. If
|
||||
this is ``False``, Streamlit sets the dataframe's width according
|
||||
to ``width``.
|
||||
|
||||
hide_index : bool or None
|
||||
Whether to hide the index column(s). If ``hide_index`` is ``None``
|
||||
(default), the visibility of index columns is automatically
|
||||
determined based on the data.
|
||||
|
||||
column_order : Iterable of str or None
|
||||
The ordered list of columns to display. If ``column_order`` is
|
||||
``None`` (default), Streamlit displays all columns in the order
|
||||
inherited from the underlying data structure. If ``column_order``
|
||||
is a list, the indicated columns will display in the order they
|
||||
appear within the list. Columns may be omitted or repeated within
|
||||
the list.
|
||||
|
||||
For example, ``column_order=("col2", "col1")`` will display
|
||||
``"col2"`` first, followed by ``"col1"``, and will hide all other
|
||||
non-index columns.
|
||||
|
||||
column_config : dict or None
|
||||
Configuration to customize how columns display. If ``column_config``
|
||||
is ``None`` (default), columns are styled based on the underlying
|
||||
data type of each column.
|
||||
|
||||
Column configuration can modify column names, visibility, type,
|
||||
width, or format, among other things. ``column_config`` must be a
|
||||
dictionary where each key is a column name and the associated value
|
||||
is one of the following:
|
||||
|
||||
- ``None``: Streamlit hides the column.
|
||||
|
||||
- A string: Streamlit changes the display label of the column to
|
||||
the given string.
|
||||
|
||||
- A column type within ``st.column_config``: Streamlit applies the
|
||||
defined configuration to the column. For example, use
|
||||
``st.column_config.NumberColumn("Dollar values”, format=”$ %d")``
|
||||
to change the displayed name of the column to "Dollar values"
|
||||
and add a "$" prefix in each cell. For more info on the
|
||||
available column types and config options, see
|
||||
`Column configuration <https://docs.streamlit.io/develop/api-reference/data/st.column_config>`_.
|
||||
|
||||
To configure the index column(s), use ``_index`` as the column name.
|
||||
|
||||
key : str
|
||||
An optional string to use for giving this element a stable
|
||||
identity. If ``key`` is ``None`` (default), this element's identity
|
||||
will be determined based on the values of the other parameters.
|
||||
|
||||
Additionally, if selections are activated and ``key`` is provided,
|
||||
Streamlit will register the key in Session State to store the
|
||||
selection state. The selection state is read-only.
|
||||
|
||||
on_select : "ignore" or "rerun" or callable
|
||||
How the dataframe should respond to user selection events. This
|
||||
controls whether or not the dataframe behaves like an input widget.
|
||||
``on_select`` can be one of the following:
|
||||
|
||||
- ``"ignore"`` (default): Streamlit will not react to any selection
|
||||
events in the dataframe. The dataframe will not behave like an
|
||||
input widget.
|
||||
|
||||
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
||||
rows or columns in the dataframe. In this case, ``st.dataframe``
|
||||
will return the selection data as a dictionary.
|
||||
|
||||
- A ``callable``: Streamlit will rerun the app and execute the
|
||||
``callable`` as a callback function before the rest of the app.
|
||||
In this case, ``st.dataframe`` will return the selection data
|
||||
as a dictionary.
|
||||
|
||||
selection_mode : "single-row", "multi-row", "single-column", \
|
||||
"multi-column", or Iterable of these
|
||||
The types of selections Streamlit should allow when selections are
|
||||
enabled with ``on_select``. This can be one of the following:
|
||||
|
||||
- "multi-row" (default): Multiple rows can be selected at a time.
|
||||
- "single-row": Only one row can be selected at a time.
|
||||
- "multi-column": Multiple columns can be selected at a time.
|
||||
- "single-column": Only one column can be selected at a time.
|
||||
- An ``Iterable`` of the above options: The table will allow
|
||||
selection based on the modes specified.
|
||||
|
||||
When column selections are enabled, column sorting is disabled.
|
||||
|
||||
row_height : int or None
|
||||
The height of each row in the dataframe in pixels. If ``row_height``
|
||||
is ``None`` (default), Streamlit will use a default row height,
|
||||
which fits one line of text.
|
||||
|
||||
Returns
|
||||
-------
|
||||
element or dict
|
||||
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
||||
internal placeholder for the dataframe element that can be used
|
||||
with the ``.add_rows()`` method. Otherwise, this command returns a
|
||||
dictionary-like object that supports both key and attribute
|
||||
notation. The attributes are described by the ``DataframeState``
|
||||
dictionary schema.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Display a dataframe**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(np.random.randn(50, 20), columns=("col %d" % i for i in range(20)))
|
||||
>>>
|
||||
>>> st.dataframe(df) # Same as st.write(df)
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe.streamlit.app/
|
||||
height: 500px
|
||||
|
||||
**Example 2: Use Pandas Styler**
|
||||
|
||||
You can also pass a Pandas Styler object to change the style of
|
||||
the rendered DataFrame:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(np.random.randn(10, 20), columns=("col %d" % i for i in range(20)))
|
||||
>>>
|
||||
>>> st.dataframe(df.style.highlight_max(axis=0))
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe1.streamlit.app/
|
||||
height: 500px
|
||||
|
||||
**Example 3: Use column configuration**
|
||||
|
||||
You can customize a dataframe via ``column_config``, ``hide_index``, or ``column_order``.
|
||||
|
||||
>>> import random
|
||||
>>> import pandas as pd
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> {
|
||||
>>> "name": ["Roadmap", "Extras", "Issues"],
|
||||
>>> "url": ["https://roadmap.streamlit.app", "https://extras.streamlit.app", "https://issues.streamlit.app"],
|
||||
>>> "stars": [random.randint(0, 1000) for _ in range(3)],
|
||||
>>> "views_history": [[random.randint(0, 5000) for _ in range(30)] for _ in range(3)],
|
||||
>>> }
|
||||
>>> )
|
||||
>>> st.dataframe(
|
||||
>>> df,
|
||||
>>> column_config={
|
||||
>>> "name": "App name",
|
||||
>>> "stars": st.column_config.NumberColumn(
|
||||
>>> "Github Stars",
|
||||
>>> help="Number of stars on GitHub",
|
||||
>>> format="%d ⭐",
|
||||
>>> ),
|
||||
>>> "url": st.column_config.LinkColumn("App URL"),
|
||||
>>> "views_history": st.column_config.LineChartColumn(
|
||||
>>> "Views (past 30 days)", y_min=0, y_max=5000
|
||||
>>> ),
|
||||
>>> },
|
||||
>>> hide_index=True,
|
||||
>>> )
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe-config.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
**Example 4: Customize your index**
|
||||
|
||||
You can use column configuration to format your index.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> from datetime import date
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> {
|
||||
>>> "Date": [date(2024, 1, 1), date(2024, 2, 1), date(2024, 3, 1)],
|
||||
>>> "Total": [13429, 23564, 23452],
|
||||
>>> }
|
||||
>>> )
|
||||
>>> df.set_index("Date", inplace=True)
|
||||
>>>
|
||||
>>> config = {
|
||||
>>> "_index": st.column_config.DateColumn("Month", format="MMM YYYY"),
|
||||
>>> "Total": st.column_config.NumberColumn("Total ($)"),
|
||||
>>> }
|
||||
>>>
|
||||
>>> st.dataframe(df, column_config=config)
|
||||
|
||||
.. output::
|
||||
https://doc-dataframe-config-index.streamlit.app/
|
||||
height: 225px
|
||||
|
||||
"""
|
||||
import pyarrow as pa
|
||||
|
||||
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
||||
raise StreamlitAPIException(
|
||||
f"You have passed {on_select} to `on_select`. But only 'ignore', "
|
||||
"'rerun', or a callable is supported."
|
||||
)
|
||||
|
||||
key = to_key(key)
|
||||
is_selection_activated = on_select != "ignore"
|
||||
|
||||
if is_selection_activated:
|
||||
# Run some checks that are only relevant when selections are activated
|
||||
is_callback = callable(on_select)
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
enable_check_callback_rules=is_callback,
|
||||
)
|
||||
|
||||
# Convert the user provided column config into the frontend compatible format:
|
||||
column_config_mapping = process_config_mapping(column_config)
|
||||
|
||||
proto = ArrowProto()
|
||||
|
||||
if use_container_width is None:
|
||||
# If use_container_width was not explicitly set by the user, we set
|
||||
# it to True if width was not set explicitly, and False otherwise.
|
||||
use_container_width = True if width is None else False
|
||||
|
||||
proto.use_container_width = use_container_width
|
||||
|
||||
if width:
|
||||
proto.width = width
|
||||
if height:
|
||||
proto.height = height
|
||||
|
||||
if row_height:
|
||||
proto.row_height = row_height
|
||||
|
||||
if column_order:
|
||||
proto.column_order[:] = column_order
|
||||
|
||||
proto.editing_mode = ArrowProto.EditingMode.READ_ONLY
|
||||
|
||||
if isinstance(data, pa.Table):
|
||||
# For pyarrow tables, we can just serialize the table directly
|
||||
proto.data = dataframe_util.convert_arrow_table_to_arrow_bytes(data)
|
||||
else:
|
||||
# For all other data formats, we need to convert them to a pandas.DataFrame
|
||||
# thereby, we also apply some data specific configs
|
||||
|
||||
# Determine the input data format
|
||||
data_format = dataframe_util.determine_data_format(data)
|
||||
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# If pandas.Styler uuid is not provided, a hash of the position
|
||||
# of the element will be used. This will cause a rerender of the table
|
||||
# when the position of the element is changed.
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
default_uuid = str(hash(delta_path))
|
||||
marshall_styler(proto, data, default_uuid)
|
||||
|
||||
# Convert the input data into a pandas.DataFrame
|
||||
data_df = dataframe_util.convert_anything_to_pandas_df(
|
||||
data, ensure_copy=False
|
||||
)
|
||||
apply_data_specific_configs(column_config_mapping, data_format)
|
||||
# Serialize the data to bytes:
|
||||
proto.data = dataframe_util.convert_pandas_df_to_arrow_bytes(data_df)
|
||||
|
||||
if hide_index is not None:
|
||||
update_column_config(
|
||||
column_config_mapping, INDEX_IDENTIFIER, {"hidden": hide_index}
|
||||
)
|
||||
marshall_column_config(proto, column_config_mapping)
|
||||
|
||||
if is_selection_activated:
|
||||
# If selection events are activated, we need to register the dataframe
|
||||
# element as a widget.
|
||||
proto.selection_mode.extend(parse_selection_mode(selection_mode))
|
||||
proto.form_id = current_form_id(self.dg)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
proto.id = compute_and_register_element_id(
|
||||
"dataframe",
|
||||
user_key=key,
|
||||
form_id=proto.form_id,
|
||||
data=proto.data,
|
||||
width=width,
|
||||
height=height,
|
||||
use_container_width=use_container_width,
|
||||
column_order=proto.column_order,
|
||||
column_config=proto.columns,
|
||||
selection_mode=selection_mode,
|
||||
is_selection_activated=is_selection_activated,
|
||||
row_height=row_height,
|
||||
)
|
||||
|
||||
serde = DataframeSelectionSerde()
|
||||
widget_state = register_widget(
|
||||
proto.id,
|
||||
on_change_handler=on_select if callable(on_select) else None,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="string_value",
|
||||
)
|
||||
self.dg._enqueue("arrow_data_frame", proto)
|
||||
return cast("DataframeState", widget_state.value)
|
||||
else:
|
||||
return self.dg._enqueue("arrow_data_frame", proto)
|
||||
|
||||
@gather_metrics("table")
|
||||
def table(self, data: Data = None) -> DeltaGenerator:
|
||||
"""Display a static table.
|
||||
|
||||
While ``st.dataframe`` is geared towards large datasets and interactive
|
||||
data exploration, ``st.table`` is useful for displaying small, styled
|
||||
tables without sorting or scrolling. For example, ``st.table`` may be
|
||||
the preferred way to display a confusion matrix or leaderboard.
|
||||
Additionally, ``st.table`` supports Markdown.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : Anything supported by st.dataframe
|
||||
The table data.
|
||||
|
||||
All cells including the index and column headers can optionally
|
||||
contain GitHub-flavored Markdown. Syntax information can be found
|
||||
at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Display a simple dataframe as a static table**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... np.random.randn(10, 5), columns=("col %d" % i for i in range(5))
|
||||
... )
|
||||
>>>
|
||||
>>> st.table(df)
|
||||
|
||||
.. output::
|
||||
https://doc-table.streamlit.app/
|
||||
height: 480px
|
||||
|
||||
**Example 2: Display a table of Markdown strings**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... {
|
||||
... "Command": ["**st.table**", "*st.dataframe*"],
|
||||
... "Type": ["`static`", "`interactive`"],
|
||||
... "Docs": [
|
||||
... "[:rainbow[docs]](https://docs.streamlit.io/develop/api-reference/data/st.dataframe)",
|
||||
... "[:book:](https://docs.streamlit.io/develop/api-reference/data/st.table)",
|
||||
... ],
|
||||
... }
|
||||
... )
|
||||
>>> st.table(df)
|
||||
|
||||
.. output::
|
||||
https://doc-table-markdown.streamlit.app/
|
||||
height: 200px
|
||||
"""
|
||||
|
||||
# Check if data is uncollected, and collect it but with 100 rows max, instead of
|
||||
# 10k rows, which is done in all other cases.
|
||||
# We use 100 rows in st.table, because large tables render slowly,
|
||||
# take too much screen space, and can crush the app.
|
||||
if dataframe_util.is_unevaluated_data_object(data):
|
||||
data = dataframe_util.convert_anything_to_pandas_df(
|
||||
data, max_unevaluated_rows=100
|
||||
)
|
||||
|
||||
# If pandas.Styler uuid is not provided, a hash of the position
|
||||
# of the element will be used. This will cause a rerender of the table
|
||||
# when the position of the element is changed.
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
default_uuid = str(hash(delta_path))
|
||||
|
||||
proto = ArrowProto()
|
||||
marshall(proto, data, default_uuid)
|
||||
return self.dg._enqueue("arrow_table", proto)
|
||||
|
||||
@gather_metrics("add_rows")
|
||||
def add_rows(self, data: Data = None, **kwargs) -> DeltaGenerator | None:
|
||||
"""Concatenate a dataframe to the bottom of the current one.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, pyspark.sql.DataFrame, snowflake.snowpark.dataframe.DataFrame, Iterable, dict, or None
|
||||
Table to concat. Optional.
|
||||
|
||||
**kwargs : pandas.DataFrame, numpy.ndarray, Iterable, dict, or None
|
||||
The named dataset to concat. Optional. You can only pass in 1
|
||||
dataset (including the one in the data parameter).
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df1 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>>
|
||||
>>> my_table = st.table(df1)
|
||||
>>>
|
||||
>>> df2 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>>
|
||||
>>> my_table.add_rows(df2)
|
||||
>>> # Now the table shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
You can do the same thing with plots. For example, if you want to add
|
||||
more data to a line chart:
|
||||
|
||||
>>> # Assuming df1 and df2 from the example above still exist...
|
||||
>>> my_chart = st.line_chart(df1)
|
||||
>>> my_chart.add_rows(df2)
|
||||
>>> # Now the chart shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
And for plots whose datasets are named, you can pass the data with a
|
||||
keyword argument where the key is the name:
|
||||
|
||||
>>> my_chart = st.vega_lite_chart(
|
||||
... {
|
||||
... "mark": "line",
|
||||
... "encoding": {"x": "a", "y": "b"},
|
||||
... "datasets": {
|
||||
... "some_fancy_name": df1, # <-- named dataset
|
||||
... },
|
||||
... "data": {"name": "some_fancy_name"},
|
||||
... }
|
||||
... )
|
||||
>>> my_chart.add_rows(some_fancy_name=df2) # <-- name used as keyword
|
||||
|
||||
"""
|
||||
return _arrow_add_rows(self.dg, data, **kwargs)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _prep_data_for_add_rows(
|
||||
data: Data,
|
||||
add_rows_metadata: AddRowsMetadata | None,
|
||||
) -> tuple[Data, AddRowsMetadata | None]:
|
||||
if not add_rows_metadata:
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# When calling add_rows on st.table or st.dataframe we want styles to
|
||||
# pass through.
|
||||
return data, None
|
||||
return dataframe_util.convert_anything_to_pandas_df(data), None
|
||||
|
||||
# If add_rows_metadata is set, it indicates that the add_rows used called
|
||||
# on a chart based on our built-in chart commands.
|
||||
|
||||
# For built-in chart commands we have to reshape the data structure
|
||||
# otherwise the input data and the actual data used
|
||||
# by vega_lite will be different, and it will throw an error.
|
||||
from streamlit.elements.lib.built_in_chart_utils import prep_chart_data_for_add_rows
|
||||
|
||||
return prep_chart_data_for_add_rows(data, add_rows_metadata)
|
||||
|
||||
|
||||
def _arrow_add_rows(
|
||||
dg: DeltaGenerator,
|
||||
data: Data = None,
|
||||
**kwargs: (
|
||||
DataFrame | npt.NDArray[Any] | Iterable[Any] | dict[Hashable, Any] | None
|
||||
),
|
||||
) -> DeltaGenerator | None:
|
||||
"""Concatenate a dataframe to the bottom of the current one.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pandas.DataFrame, pandas.Styler, numpy.ndarray, Iterable, dict, or None
|
||||
Table to concat. Optional.
|
||||
|
||||
**kwargs : pandas.DataFrame, numpy.ndarray, Iterable, dict, or None
|
||||
The named dataset to concat. Optional. You can only pass in 1
|
||||
dataset (including the one in the data parameter).
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df1 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>> my_table = st.table(df1)
|
||||
>>>
|
||||
>>> df2 = pd.DataFrame(
|
||||
... np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
|
||||
... )
|
||||
>>> my_table.add_rows(df2)
|
||||
>>> # Now the table shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
You can do the same thing with plots. For example, if you want to add
|
||||
more data to a line chart:
|
||||
|
||||
>>> # Assuming df1 and df2 from the example above still exist...
|
||||
>>> my_chart = st.line_chart(df1)
|
||||
>>> my_chart.add_rows(df2)
|
||||
>>> # Now the chart shown in the Streamlit app contains the data for
|
||||
>>> # df1 followed by the data for df2.
|
||||
|
||||
And for plots whose datasets are named, you can pass the data with a
|
||||
keyword argument where the key is the name:
|
||||
|
||||
>>> my_chart = st.vega_lite_chart(
|
||||
... {
|
||||
... "mark": "line",
|
||||
... "encoding": {"x": "a", "y": "b"},
|
||||
... "datasets": {
|
||||
... "some_fancy_name": df1, # <-- named dataset
|
||||
... },
|
||||
... "data": {"name": "some_fancy_name"},
|
||||
... }
|
||||
... )
|
||||
>>> my_chart.add_rows(some_fancy_name=df2) # <-- name used as keyword
|
||||
|
||||
"""
|
||||
if dg._root_container is None or dg._cursor is None:
|
||||
return dg
|
||||
|
||||
if not dg._cursor.is_locked:
|
||||
raise StreamlitAPIException("Only existing elements can `add_rows`.")
|
||||
|
||||
# Accept syntax st._arrow_add_rows(df).
|
||||
if data is not None and len(kwargs) == 0:
|
||||
name = ""
|
||||
# Accept syntax st._arrow_add_rows(foo=df).
|
||||
elif len(kwargs) == 1:
|
||||
name, data = kwargs.popitem()
|
||||
# Raise error otherwise.
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Wrong number of arguments to add_rows()."
|
||||
"Command requires exactly one dataset"
|
||||
)
|
||||
|
||||
# When doing _arrow_add_rows on an element that does not already have data
|
||||
# (for example, st.line_chart() without any args), call the original
|
||||
# st.foo() element with new data instead of doing a _arrow_add_rows().
|
||||
if (
|
||||
"add_rows_metadata" in dg._cursor.props
|
||||
and dg._cursor.props["add_rows_metadata"]
|
||||
and dg._cursor.props["add_rows_metadata"].last_index is None
|
||||
):
|
||||
st_method = getattr(dg, dg._cursor.props["add_rows_metadata"].chart_command)
|
||||
st_method(data, **kwargs)
|
||||
return None
|
||||
|
||||
new_data, dg._cursor.props["add_rows_metadata"] = _prep_data_for_add_rows(
|
||||
data,
|
||||
dg._cursor.props["add_rows_metadata"],
|
||||
)
|
||||
|
||||
msg = ForwardMsg()
|
||||
msg.metadata.delta_path[:] = dg._cursor.delta_path
|
||||
|
||||
default_uuid = str(hash(dg._get_delta_path_str()))
|
||||
marshall(msg.delta.arrow_add_rows.data, new_data, default_uuid)
|
||||
|
||||
if name:
|
||||
msg.delta.arrow_add_rows.name = name
|
||||
msg.delta.arrow_add_rows.has_name = True
|
||||
|
||||
enqueue_message(msg)
|
||||
|
||||
return dg
|
||||
|
||||
|
||||
def marshall(proto: ArrowProto, data: Data, default_uuid: str | None = None) -> None:
|
||||
"""Marshall pandas.DataFrame into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
data : pandas.DataFrame, pandas.Styler, pyarrow.Table, numpy.ndarray, pyspark.sql.DataFrame, snowflake.snowpark.DataFrame, Iterable, dict, or None
|
||||
Something that is or can be converted to a dataframe.
|
||||
|
||||
default_uuid : str | None
|
||||
If pandas.Styler UUID is not provided, this value will be used.
|
||||
This attribute is optional and only used for pandas.Styler, other elements
|
||||
(e.g. charts) can ignore it.
|
||||
|
||||
"""
|
||||
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# default_uuid is a string only if the data is a `Styler`,
|
||||
# and `None` otherwise.
|
||||
assert isinstance(default_uuid, str), (
|
||||
"Default UUID must be a string for Styler data."
|
||||
)
|
||||
marshall_styler(proto, data, default_uuid)
|
||||
|
||||
proto.data = dataframe_util.convert_anything_to_arrow_bytes(data)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Balloons_pb2 import Balloons as BalloonsProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class BalloonsMixin:
|
||||
@gather_metrics("balloons")
|
||||
def balloons(self) -> DeltaGenerator:
|
||||
"""Draw celebratory balloons.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.balloons()
|
||||
|
||||
...then watch your app and get ready for a celebration!
|
||||
|
||||
"""
|
||||
balloons_proto = BalloonsProto()
|
||||
balloons_proto.show = True
|
||||
return self.dg._enqueue("balloons", balloons_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A Python wrapper around Bokeh."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Final, cast
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.BokehChart_pb2 import BokehChart as BokehChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bokeh.plotting.figure import Figure
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
ST_BOKEH_VERSION: Final = "2.4.3"
|
||||
|
||||
|
||||
class BokehMixin:
|
||||
@gather_metrics("bokeh_chart")
|
||||
def bokeh_chart(
|
||||
self,
|
||||
figure: Figure,
|
||||
use_container_width: bool = True,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an interactive Bokeh chart.
|
||||
|
||||
Bokeh is a charting library for Python. The arguments to this function
|
||||
closely follow the ones for Bokeh's ``show`` function. You can find
|
||||
more about Bokeh at https://bokeh.pydata.org.
|
||||
|
||||
To show Bokeh charts in Streamlit, call ``st.bokeh_chart``
|
||||
wherever you would call Bokeh's ``show``.
|
||||
|
||||
.. Important::
|
||||
You must install ``bokeh==2.4.3`` and ``numpy<2`` to use this
|
||||
command.
|
||||
|
||||
If you need a newer version of Bokeh, use our |streamlit-bokeh|_
|
||||
custom component instead.
|
||||
|
||||
.. |streamlit-bokeh| replace:: ``streamlit-bokeh``
|
||||
.. _streamlit-bokeh: https://github.com/streamlit/streamlit-bokeh
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure : bokeh.plotting.figure.Figure
|
||||
A Bokeh figure to plot.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True`` (default),
|
||||
Streamlit sets the width of the figure to match the width of the parent
|
||||
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
||||
width of the chart to fit its contents according to the plotting library,
|
||||
up to the width of the parent container.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> from bokeh.plotting import figure
|
||||
>>>
|
||||
>>> x = [1, 2, 3, 4, 5]
|
||||
>>> y = [6, 7, 2, 4, 5]
|
||||
>>>
|
||||
>>> p = figure(title="simple line example", x_axis_label="x", y_axis_label="y")
|
||||
>>> p.line(x, y, legend_label="Trend", line_width=2)
|
||||
>>>
|
||||
>>> st.bokeh_chart(p)
|
||||
|
||||
.. output::
|
||||
https://doc-bokeh-chart.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
"""
|
||||
import bokeh
|
||||
|
||||
if bokeh.__version__ != ST_BOKEH_VERSION:
|
||||
raise StreamlitAPIException(
|
||||
f"Streamlit only supports Bokeh version {ST_BOKEH_VERSION}, "
|
||||
f"but you have version {bokeh.__version__} installed. Please "
|
||||
f"run `pip install --force-reinstall --no-deps bokeh=="
|
||||
f"{ST_BOKEH_VERSION}` to install the correct version.\n\n\n"
|
||||
f"To use the latest version of Bokeh, install our custom component, "
|
||||
f"[streamlit-bokeh](https://github.com/streamlit/streamlit-bokeh)."
|
||||
)
|
||||
|
||||
# Generate element ID from delta path
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
|
||||
element_id = calc_md5(delta_path.encode())
|
||||
bokeh_chart_proto = BokehChartProto()
|
||||
marshall(bokeh_chart_proto, figure, use_container_width, element_id)
|
||||
return self.dg._enqueue("bokeh_chart", bokeh_chart_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: BokehChartProto,
|
||||
figure: Figure,
|
||||
use_container_width: bool,
|
||||
element_id: str,
|
||||
) -> None:
|
||||
"""Construct a Bokeh chart object.
|
||||
|
||||
See DeltaGenerator.bokeh_chart for docs.
|
||||
"""
|
||||
from bokeh.embed import json_item
|
||||
|
||||
data = json_item(figure)
|
||||
proto.figure = json.dumps(data)
|
||||
proto.use_container_width = use_container_width
|
||||
proto.element_id = element_id
|
||||
114
myenv/lib/python3.11/site-packages/streamlit/elements/code.py
Normal file
114
myenv/lib/python3.11/site-packages/streamlit/elements/code.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Code_pb2 import Code as CodeProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class CodeMixin:
|
||||
@gather_metrics("code")
|
||||
def code(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
language: str | None = "python",
|
||||
*,
|
||||
line_numbers: bool = False,
|
||||
wrap_lines: bool = False,
|
||||
height: int | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a code block with optional syntax highlighting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The string to display as code or monospace text.
|
||||
|
||||
language : str or None
|
||||
The language that the code is written in, for syntax highlighting.
|
||||
This defaults to ``"python"``. If this is ``None``, the code will
|
||||
be plain, monospace text.
|
||||
|
||||
For a list of available ``language`` values, see
|
||||
`react-syntax-highlighter
|
||||
<https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_PRISM.MD>`_
|
||||
on GitHub.
|
||||
|
||||
line_numbers : bool
|
||||
An optional boolean indicating whether to show line numbers to the
|
||||
left of the code block. This defaults to ``False``.
|
||||
|
||||
wrap_lines : bool
|
||||
An optional boolean indicating whether to wrap lines. This defaults
|
||||
to ``False``.
|
||||
|
||||
height : int or None
|
||||
Desired height of the code block expressed in pixels. If ``height``
|
||||
is ``None`` (default), Streamlit sets the element's height to fit
|
||||
its content. Vertical scrolling within the element is enabled when
|
||||
the height does not accomodate all lines.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> code = '''def hello():
|
||||
... print("Hello, Streamlit!")'''
|
||||
>>> st.code(code, language="python")
|
||||
|
||||
.. output ::
|
||||
https://doc-code.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> code = '''Is it a crown or boat?
|
||||
... ii
|
||||
... iiiiii
|
||||
... WWw .iiiiiiii. ...:
|
||||
... WWWWWWw .iiiiiiiiiiii. ........
|
||||
... WWWWWWWWWWw iiiiiiiiiiiiiiii ...........
|
||||
... WWWWWWWWWWWWWWwiiiiiiiiiiiiiiiii............
|
||||
... WWWWWWWWWWWWWWWWWWwiiiiiiiiiiiiii.........
|
||||
... WWWWWWWWWWWWWWWWWWWWWWwiiiiiiiiii.......
|
||||
... WWWWWWWWWWWWWWWWWWWWWWWWWWwiiiiiii....
|
||||
... WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWwiiii.
|
||||
... -MMMWWWWWWWWWWWWWWWWWWWWWWMMM-
|
||||
... '''
|
||||
>>> st.code(code, language=None)
|
||||
|
||||
.. output ::
|
||||
https://doc-code-ascii.streamlit.app/
|
||||
height: 380px
|
||||
"""
|
||||
code_proto = CodeProto()
|
||||
code_proto.code_text = clean_text(body)
|
||||
code_proto.language = language or "plaintext"
|
||||
code_proto.show_line_numbers = line_numbers
|
||||
code_proto.wrap_lines = wrap_lines
|
||||
if height:
|
||||
code_proto.height = height
|
||||
return self.dg._enqueue("code", code_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,546 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.elements.lib.event_utils import AttributeDictionary
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import check_widget_policies
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.DeckGlJsonChart_pb2 import DeckGlJsonChart as PydeckProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetCallback,
|
||||
register_widget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
from pydeck import Deck
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
# Mapping used when no data is passed.
|
||||
EMPTY_MAP: Final[Mapping[str, Any]] = {
|
||||
"initialViewState": {"latitude": 0, "longitude": 0, "pitch": 0, "zoom": 1},
|
||||
}
|
||||
|
||||
SelectionMode: TypeAlias = Literal["single-object", "multi-object"]
|
||||
_SELECTION_MODES: Final[set[SelectionMode]] = {
|
||||
"single-object",
|
||||
"multi-object",
|
||||
}
|
||||
|
||||
|
||||
def parse_selection_mode(
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode],
|
||||
) -> set[PydeckProto.SelectionMode.ValueType]:
|
||||
"""Parse and check the user provided selection modes."""
|
||||
if isinstance(selection_mode, str):
|
||||
# Only a single selection mode was passed
|
||||
selection_mode_set = {selection_mode}
|
||||
else:
|
||||
# Multiple selection modes were passed.
|
||||
# This is not yet supported as a functionality, but the infra is here to
|
||||
# support it in the future!
|
||||
# @see DeckGlJsonChart.tsx
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. ",
|
||||
"Selection mode must be a single value, but got a set instead.",
|
||||
)
|
||||
|
||||
if not selection_mode_set.issubset(_SELECTION_MODES):
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. "
|
||||
f"Valid options are: {_SELECTION_MODES}"
|
||||
)
|
||||
|
||||
if selection_mode_set.issuperset({"single-object", "multi-object"}):
|
||||
raise StreamlitAPIException(
|
||||
"Only one of `single-object` or `multi-object` can be selected as selection mode."
|
||||
)
|
||||
|
||||
parsed_selection_modes = []
|
||||
for selection_mode in selection_mode_set:
|
||||
if selection_mode == "single-object":
|
||||
parsed_selection_modes.append(PydeckProto.SelectionMode.SINGLE_OBJECT)
|
||||
elif selection_mode == "multi-object":
|
||||
parsed_selection_modes.append(PydeckProto.SelectionMode.MULTI_OBJECT)
|
||||
return set(parsed_selection_modes)
|
||||
|
||||
|
||||
class PydeckSelectionState(TypedDict, total=False):
|
||||
r"""
|
||||
The schema for the PyDeck chart selection state.
|
||||
|
||||
The selection state is stored in a dictionary-like object that supports
|
||||
both key and attribute notation. Selection states cannot be
|
||||
programmatically changed or set through Session State.
|
||||
|
||||
You must define ``id`` in ``pydeck.Layer`` to ensure statefulness when
|
||||
using selections with ``st.pydeck_chart``.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
indices : dict[str, list[int]]
|
||||
A dictionary of selected objects by layer. Each key in the dictionary
|
||||
is a layer id, and each value is a list of object indices within that
|
||||
layer.
|
||||
objects : dict[str, list[dict[str, Any]]]
|
||||
A dictionary of object attributes by layer. Each key in the dictionary
|
||||
is a layer id, and each value is a list of metadata dictionaries for
|
||||
the selected objects in that layer.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following example has multi-object selection enabled. The chart
|
||||
displays US state capitals by population (2023 US Census estimate). You
|
||||
can access this `data
|
||||
<https://github.com/streamlit/docs/blob/main/python/api-examples-source/data/capitals.csv>`_
|
||||
from GitHub.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pydeck
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> capitals = pd.read_csv(
|
||||
... "capitals.csv",
|
||||
... header=0,
|
||||
... names=[
|
||||
... "Capital",
|
||||
... "State",
|
||||
... "Abbreviation",
|
||||
... "Latitude",
|
||||
... "Longitude",
|
||||
... "Population",
|
||||
... ],
|
||||
... )
|
||||
>>> capitals["size"] = capitals.Population / 10
|
||||
>>>
|
||||
>>> point_layer = pydeck.Layer(
|
||||
... "ScatterplotLayer",
|
||||
... data=capitals,
|
||||
... id="capital-cities",
|
||||
... get_position=["Longitude", "Latitude"],
|
||||
... get_color="[255, 75, 75]",
|
||||
... pickable=True,
|
||||
... auto_highlight=True,
|
||||
... get_radius="size",
|
||||
... )
|
||||
>>>
|
||||
>>> view_state = pydeck.ViewState(
|
||||
... latitude=40, longitude=-117, controller=True, zoom=2.4, pitch=30
|
||||
... )
|
||||
>>>
|
||||
>>> chart = pydeck.Deck(
|
||||
... point_layer,
|
||||
... initial_view_state=view_state,
|
||||
... tooltip={"text": "{Capital}, {Abbreviation}\nPopulation: {Population}"},
|
||||
... )
|
||||
>>>
|
||||
>>> event = st.pydeck_chart(chart, on_select="rerun", selection_mode="multi-object")
|
||||
>>>
|
||||
>>> event.selection
|
||||
|
||||
.. output ::
|
||||
https://doc-pydeck-event-state-selections.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
This is an example of the selection state when selecting a single object
|
||||
from a layer with id, ``"captial-cities"``:
|
||||
|
||||
>>> {
|
||||
>>> "indices":{
|
||||
>>> "capital-cities":[
|
||||
>>> 2
|
||||
>>> ]
|
||||
>>> },
|
||||
>>> "objects":{
|
||||
>>> "capital-cities":[
|
||||
>>> {
|
||||
>>> "Abbreviation":" AZ"
|
||||
>>> "Capital":"Phoenix"
|
||||
>>> "Latitude":33.448457
|
||||
>>> "Longitude":-112.073844
|
||||
>>> "Population":1650070
|
||||
>>> "State":" Arizona"
|
||||
>>> "size":165007.0
|
||||
>>> }
|
||||
>>> ]
|
||||
>>> }
|
||||
>>> }
|
||||
|
||||
"""
|
||||
|
||||
indices: dict[str, list[int]]
|
||||
objects: dict[str, list[dict[str, Any]]]
|
||||
|
||||
|
||||
class PydeckState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the PyDeck event state.
|
||||
|
||||
The event state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Event states cannot be programmatically changed
|
||||
or set through Session State.
|
||||
|
||||
Only selection events are supported at this time.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
selection : dict
|
||||
The state of the ``on_select`` event. This attribute returns a
|
||||
dictionary-like object that supports both key and attribute notation.
|
||||
The attributes are described by the ``PydeckSelectionState``
|
||||
dictionary schema.
|
||||
|
||||
"""
|
||||
|
||||
selection: PydeckSelectionState
|
||||
|
||||
|
||||
@dataclass
|
||||
class PydeckSelectionSerde:
|
||||
"""PydeckSelectionSerde is used to serialize and deserialize the Pydeck selection state."""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> PydeckState:
|
||||
empty_selection_state: PydeckState = {
|
||||
"selection": {
|
||||
"indices": {},
|
||||
"objects": {},
|
||||
}
|
||||
}
|
||||
|
||||
selection_state = (
|
||||
empty_selection_state if ui_value is None else json.loads(ui_value)
|
||||
)
|
||||
|
||||
# We have seen some situations where the ui_value was just an empty
|
||||
# dict, so we want to ensure that it always returns the empty state in
|
||||
# case this happens.
|
||||
if "selection" not in selection_state:
|
||||
selection_state = empty_selection_state
|
||||
|
||||
return cast("PydeckState", AttributeDictionary(selection_state))
|
||||
|
||||
def serialize(self, selection_state: PydeckState) -> str:
|
||||
return json.dumps(selection_state, default=str)
|
||||
|
||||
|
||||
class PydeckMixin:
|
||||
@overload
|
||||
def pydeck_chart(
|
||||
self,
|
||||
pydeck_obj: Deck | None = None,
|
||||
*,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
selection_mode: Literal[
|
||||
"single-object"
|
||||
], # Selection mode will only be activated by on_select param, this is a default value here to make it work with mypy
|
||||
on_select: Literal["ignore"], # No default value here to make it work with mypy
|
||||
key: Key | None = None,
|
||||
) -> DeltaGenerator: ...
|
||||
|
||||
@overload
|
||||
def pydeck_chart(
|
||||
self,
|
||||
pydeck_obj: Deck | None = None,
|
||||
*,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
selection_mode: SelectionMode = "single-object",
|
||||
on_select: Literal["rerun"] | WidgetCallback = "rerun",
|
||||
key: Key | None = None,
|
||||
) -> PydeckState: ...
|
||||
|
||||
@gather_metrics("pydeck_chart")
|
||||
def pydeck_chart(
|
||||
self,
|
||||
pydeck_obj: Deck | None = None,
|
||||
*,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
selection_mode: SelectionMode = "single-object",
|
||||
on_select: Literal["rerun", "ignore"] | WidgetCallback = "ignore",
|
||||
key: Key | None = None,
|
||||
) -> DeltaGenerator | PydeckState:
|
||||
"""Draw a chart using the PyDeck library.
|
||||
|
||||
This supports 3D maps, point clouds, and more! More info about PyDeck
|
||||
at https://deckgl.readthedocs.io/en/latest/.
|
||||
|
||||
These docs are also quite useful:
|
||||
|
||||
- DeckGL docs: https://github.com/uber/deck.gl/tree/master/docs
|
||||
- DeckGL JSON docs: https://github.com/uber/deck.gl/tree/master/modules/json
|
||||
|
||||
When using this command, Mapbox provides the map tiles to render map
|
||||
content. Note that Mapbox is a third-party product and Streamlit accepts
|
||||
no responsibility or liability of any kind for Mapbox or for any content
|
||||
or information made available by Mapbox.
|
||||
|
||||
Mapbox requires users to register and provide a token before users can
|
||||
request map tiles. Currently, Streamlit provides this token for you, but
|
||||
this could change at any time. We strongly recommend all users create and
|
||||
use their own personal Mapbox token to avoid any disruptions to their
|
||||
experience. You can do this with the ``mapbox.token`` config option. The
|
||||
use of Mapbox is governed by Mapbox's Terms of Use.
|
||||
|
||||
To get a token for yourself, create an account at https://mapbox.com.
|
||||
For more info on how to set config options, see
|
||||
https://docs.streamlit.io/develop/api-reference/configuration/config.toml.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pydeck_obj : pydeck.Deck or None
|
||||
Object specifying the PyDeck chart to draw.
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True`` (default),
|
||||
Streamlit sets the width of the figure to match the width of the parent
|
||||
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
||||
width of the chart to fit its contents according to the plotting library,
|
||||
up to the width of the parent container.
|
||||
width : int or None
|
||||
Desired width of the chart expressed in pixels. If ``width`` is
|
||||
``None`` (default), Streamlit sets the width of the chart to fit
|
||||
its contents according to the plotting library, up to the width of
|
||||
the parent container. If ``width`` is greater than the width of the
|
||||
parent container, Streamlit sets the chart width to match the width
|
||||
of the parent container.
|
||||
|
||||
To use ``width``, you must set ``use_container_width=False``.
|
||||
height : int or None
|
||||
Desired height of the chart expressed in pixels. If ``height`` is
|
||||
``None`` (default), Streamlit sets the height of the chart to fit
|
||||
its contents according to the plotting library.
|
||||
on_select : "ignore" or "rerun" or callable
|
||||
How the figure should respond to user selection events. This controls
|
||||
whether or not the chart behaves like an input widget.
|
||||
``on_select`` can be one of the following:
|
||||
|
||||
- ``"ignore"`` (default): Streamlit will not react to any selection
|
||||
events in the chart. The figure will not behave like an
|
||||
input widget.
|
||||
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
||||
data in the chart. In this case, ``st.pydeck_chart`` will return
|
||||
the selection data as a dictionary.
|
||||
- A ``callable``: Streamlit will rerun the app and execute the callable
|
||||
as a callback function before the rest of the app. In this case,
|
||||
``st.pydeck_chart`` will return the selection data as a
|
||||
dictionary.
|
||||
|
||||
If ``on_select`` is not ``"ignore"``, all layers must have a
|
||||
declared ``id`` to keep the chart stateful across reruns.
|
||||
selection_mode : "single-object" or "multi-object"
|
||||
The selection mode of the chart. This can be one of the following:
|
||||
|
||||
- ``"single-object"`` (default): Only one object can be selected at
|
||||
a time.
|
||||
- ``"multi-object"``: Multiple objects can be selected at a time.
|
||||
|
||||
key : str
|
||||
An optional string to use for giving this element a stable
|
||||
identity. If ``key`` is ``None`` (default), this element's identity
|
||||
will be determined based on the values of the other parameters.
|
||||
|
||||
Additionally, if selections are activated and ``key`` is provided,
|
||||
Streamlit will register the key in Session State to store the
|
||||
selection state. The selection state is read-only.
|
||||
|
||||
Returns
|
||||
-------
|
||||
element or dict
|
||||
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
||||
internal placeholder for the chart element. Otherwise, this method
|
||||
returns a dictionary-like object that supports both key and
|
||||
attribute notation. The attributes are described by the
|
||||
``PydeckState`` dictionary schema.
|
||||
|
||||
Example
|
||||
-------
|
||||
Here's a chart using a HexagonLayer and a ScatterplotLayer. It uses either the
|
||||
light or dark map style, based on which Streamlit theme is currently active:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>> import pydeck as pdk
|
||||
>>>
|
||||
>>> chart_data = pd.DataFrame(
|
||||
... np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
|
||||
... columns=["lat", "lon"],
|
||||
... )
|
||||
>>>
|
||||
>>> st.pydeck_chart(
|
||||
... pdk.Deck(
|
||||
... map_style=None,
|
||||
... initial_view_state=pdk.ViewState(
|
||||
... latitude=37.76,
|
||||
... longitude=-122.4,
|
||||
... zoom=11,
|
||||
... pitch=50,
|
||||
... ),
|
||||
... layers=[
|
||||
... pdk.Layer(
|
||||
... "HexagonLayer",
|
||||
... data=chart_data,
|
||||
... get_position="[lon, lat]",
|
||||
... radius=200,
|
||||
... elevation_scale=4,
|
||||
... elevation_range=[0, 1000],
|
||||
... pickable=True,
|
||||
... extruded=True,
|
||||
... ),
|
||||
... pdk.Layer(
|
||||
... "ScatterplotLayer",
|
||||
... data=chart_data,
|
||||
... get_position="[lon, lat]",
|
||||
... get_color="[200, 30, 0, 160]",
|
||||
... get_radius=200,
|
||||
... ),
|
||||
... ],
|
||||
... )
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-pydeck-chart.streamlit.app/
|
||||
height: 530px
|
||||
|
||||
.. note::
|
||||
To make the PyDeck chart's style consistent with Streamlit's theme,
|
||||
you can set ``map_style=None`` in the ``pydeck.Deck`` object.
|
||||
|
||||
"""
|
||||
pydeck_proto = PydeckProto()
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if pydeck_obj is None:
|
||||
spec = json.dumps(EMPTY_MAP)
|
||||
else:
|
||||
spec = pydeck_obj.to_json()
|
||||
|
||||
pydeck_proto.json = spec
|
||||
pydeck_proto.use_container_width = use_container_width
|
||||
|
||||
if width:
|
||||
pydeck_proto.width = width
|
||||
if height:
|
||||
pydeck_proto.height = height
|
||||
|
||||
tooltip = _get_pydeck_tooltip(pydeck_obj)
|
||||
if tooltip:
|
||||
pydeck_proto.tooltip = json.dumps(tooltip)
|
||||
|
||||
mapbox_token = config.get_option("mapbox.token")
|
||||
if mapbox_token:
|
||||
pydeck_proto.mapbox_token = mapbox_token
|
||||
|
||||
key = to_key(key)
|
||||
is_selection_activated = on_select != "ignore"
|
||||
|
||||
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
||||
raise StreamlitAPIException(
|
||||
f"You have passed {on_select} to `on_select`. But only 'ignore', 'rerun', or a callable is supported."
|
||||
)
|
||||
|
||||
if is_selection_activated:
|
||||
# Selections are activated, treat Pydeck as a widget:
|
||||
pydeck_proto.selection_mode.extend(parse_selection_mode(selection_mode))
|
||||
|
||||
# Run some checks that are only relevant when selections are activated
|
||||
is_callback = callable(on_select)
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
enable_check_callback_rules=is_callback,
|
||||
)
|
||||
pydeck_proto.form_id = current_form_id(self.dg)
|
||||
|
||||
pydeck_proto.id = compute_and_register_element_id(
|
||||
"deck_gl_json_chart",
|
||||
user_key=key,
|
||||
is_selection_activated=is_selection_activated,
|
||||
selection_mode=selection_mode,
|
||||
use_container_width=use_container_width,
|
||||
spec=spec,
|
||||
form_id=pydeck_proto.form_id,
|
||||
)
|
||||
|
||||
serde = PydeckSelectionSerde()
|
||||
|
||||
widget_state = register_widget(
|
||||
pydeck_proto.id,
|
||||
ctx=ctx,
|
||||
deserializer=serde.deserialize,
|
||||
on_change_handler=on_select if callable(on_select) else None,
|
||||
serializer=serde.serialize,
|
||||
value_type="string_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("deck_gl_json_chart", pydeck_proto)
|
||||
|
||||
return cast("PydeckState", widget_state.value)
|
||||
|
||||
return self.dg._enqueue("deck_gl_json_chart", pydeck_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _get_pydeck_tooltip(pydeck_obj: Deck | None) -> dict[str, str] | None:
|
||||
if pydeck_obj is None:
|
||||
return None
|
||||
|
||||
# For pydeck <0.8.1 or pydeck>=0.8.1 when jupyter extra is installed.
|
||||
desk_widget = getattr(pydeck_obj, "deck_widget", None)
|
||||
if desk_widget is not None and isinstance(desk_widget.tooltip, dict):
|
||||
return desk_widget.tooltip
|
||||
|
||||
# For pydeck >=0.8.1 when jupyter extra is not installed.
|
||||
# For details, see: https://github.com/visgl/deck.gl/pull/7125/files
|
||||
tooltip = getattr(pydeck_obj, "_tooltip", None)
|
||||
if tooltip is not None and isinstance(tooltip, dict):
|
||||
return cast("dict[str, str]", tooltip)
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,267 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Callable, TypeVar, cast, overload
|
||||
|
||||
from streamlit.delta_generator_singletons import (
|
||||
get_dg_singleton_instance,
|
||||
get_last_dg_added_to_context_stack,
|
||||
)
|
||||
from streamlit.deprecation_util import (
|
||||
make_deprecated_name_warning,
|
||||
show_deprecation_warning,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.fragment import _fragment
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.elements.lib.dialog import DialogWidth
|
||||
|
||||
|
||||
def _assert_no_nested_dialogs() -> None:
|
||||
"""Check the current stack for existing DeltaGenerator's of type 'dialog'.
|
||||
Note that the check like this only works when Dialog is called as a context manager,
|
||||
as this populates the dg_stack in delta_generator correctly.
|
||||
|
||||
This does not detect the edge case in which someone calls, for example,
|
||||
`with st.sidebar` inside of a dialog function and opens a dialog in there, as
|
||||
`with st.sidebar` pushes the new DeltaGenerator to the stack. In order to check for
|
||||
that edge case, we could try to check all DeltaGenerators in the stack, and not only
|
||||
the last one. Since we deem this to be an edge case, we lean towards simplicity
|
||||
here.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException
|
||||
Raised if the user tries to nest dialogs inside of each other.
|
||||
"""
|
||||
last_dg_in_current_context = get_last_dg_added_to_context_stack()
|
||||
if last_dg_in_current_context and "dialog" in set(
|
||||
last_dg_in_current_context._ancestor_block_types
|
||||
):
|
||||
raise StreamlitAPIException("Dialogs may not be nested inside other dialogs.")
|
||||
|
||||
|
||||
F = TypeVar("F", bound=Callable[..., None])
|
||||
|
||||
|
||||
def _dialog_decorator(
|
||||
non_optional_func: F,
|
||||
title: str,
|
||||
*,
|
||||
width: DialogWidth = "small",
|
||||
should_show_deprecation_warning: bool = False,
|
||||
) -> F:
|
||||
if title is None or title == "":
|
||||
raise StreamlitAPIException(
|
||||
"A non-empty `title` argument has to be provided for dialogs, for example "
|
||||
'`@st.dialog("Example Title")`.'
|
||||
)
|
||||
|
||||
@wraps(non_optional_func)
|
||||
def wrap(*args, **kwargs) -> None:
|
||||
_assert_no_nested_dialogs()
|
||||
# Call the Dialog on the event_dg because it lives outside of the normal
|
||||
# Streamlit UI flow. For example, if it is called from the sidebar, it should
|
||||
# not inherit the sidebar theming.
|
||||
dialog = get_dg_singleton_instance().event_dg._dialog(
|
||||
title=title, dismissible=True, width=width
|
||||
)
|
||||
dialog.open()
|
||||
|
||||
def dialog_content() -> None:
|
||||
if should_show_deprecation_warning:
|
||||
show_deprecation_warning(
|
||||
make_deprecated_name_warning(
|
||||
"experimental_dialog",
|
||||
"dialog",
|
||||
"2025-01-01",
|
||||
)
|
||||
)
|
||||
|
||||
# if the dialog should be closed, st.rerun() has to be called
|
||||
# (same behavior as with st.fragment)
|
||||
_ = non_optional_func(*args, **kwargs)
|
||||
return None
|
||||
|
||||
# the fragment decorator has multiple return types so that you can pass
|
||||
# arguments to it. Here we know the return type, so we cast
|
||||
fragmented_dialog_content = cast(
|
||||
"Callable[[], None]",
|
||||
_fragment(
|
||||
dialog_content, additional_hash_info=non_optional_func.__qualname__
|
||||
),
|
||||
)
|
||||
|
||||
with dialog:
|
||||
fragmented_dialog_content()
|
||||
return None
|
||||
|
||||
return cast("F", wrap)
|
||||
|
||||
|
||||
@overload
|
||||
def dialog_decorator(
|
||||
title: str, *, width: DialogWidth = "small"
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
# 'title' can be a function since `dialog_decorator` is a decorator.
|
||||
# We just call it 'title' here though to make the user-doc more friendly as
|
||||
# we want the user to pass a title, not a function. The user is supposed to
|
||||
# call it like @st.dialog("my_title") , which makes 'title' a positional arg, hence
|
||||
# this 'trick'. The overload is required to have a good type hint for the decorated
|
||||
# function args.
|
||||
@overload
|
||||
def dialog_decorator(title: F, *, width: DialogWidth = "small") -> F: ...
|
||||
|
||||
|
||||
@gather_metrics("dialog")
|
||||
def dialog_decorator(
|
||||
title: F | str, *, width: DialogWidth = "small"
|
||||
) -> F | Callable[[F], F]:
|
||||
"""Function decorator to create a modal dialog.
|
||||
|
||||
A function decorated with ``@st.dialog`` becomes a dialog
|
||||
function. When you call a dialog function, Streamlit inserts a modal dialog
|
||||
into your app. Streamlit element commands called within the dialog function
|
||||
render inside the modal dialog.
|
||||
|
||||
The dialog function can accept arguments that can be passed when it is
|
||||
called. Any values from the dialog that need to be accessed from the wider
|
||||
app should generally be stored in Session State.
|
||||
|
||||
A user can dismiss a modal dialog by clicking outside of it, clicking the
|
||||
"**X**" in its upper-right corner, or pressing ``ESC`` on their keyboard.
|
||||
Dismissing a modal dialog does not trigger an app rerun. To close the modal
|
||||
dialog programmatically, call ``st.rerun()`` explicitly inside of the
|
||||
dialog function.
|
||||
|
||||
``st.dialog`` inherits behavior from |st.fragment|_.
|
||||
When a user interacts with an input widget created inside a dialog function,
|
||||
Streamlit only reruns the dialog function instead of the full script.
|
||||
|
||||
Calling ``st.sidebar`` in a dialog function is not supported.
|
||||
|
||||
Dialog code can interact with Session State, imported modules, and other
|
||||
Streamlit elements created outside the dialog. Note that these interactions
|
||||
are additive across multiple dialog reruns. You are responsible for
|
||||
handling any side effects of that behavior.
|
||||
|
||||
.. warning::
|
||||
Only one dialog function may be called in a script run, which means
|
||||
that only one dialog can be open at any given time.
|
||||
|
||||
.. |st.fragment| replace:: ``st.fragment``
|
||||
.. _st.fragment: https://docs.streamlit.io/develop/api-reference/execution-flow/st.fragment
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title to display at the top of the modal dialog. It cannot be empty.
|
||||
width : "small", "large"
|
||||
The width of the modal dialog. If ``width`` is ``"small`` (default), the
|
||||
modal dialog will be 500 pixels wide. If ``width`` is ``"large"``, the
|
||||
modal dialog will be about 750 pixels wide.
|
||||
|
||||
Examples
|
||||
--------
|
||||
The following example demonstrates the basic usage of ``@st.dialog``.
|
||||
In this app, clicking "**A**" or "**B**" will open a modal dialog and prompt you
|
||||
to enter a reason for your vote. In the modal dialog, click "**Submit**" to record
|
||||
your vote into Session State and rerun the app. This will close the modal dialog
|
||||
since the dialog function is not called during the full-script rerun.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> @st.dialog("Cast your vote")
|
||||
>>> def vote(item):
|
||||
>>> st.write(f"Why is {item} your favorite?")
|
||||
>>> reason = st.text_input("Because...")
|
||||
>>> if st.button("Submit"):
|
||||
>>> st.session_state.vote = {"item": item, "reason": reason}
|
||||
>>> st.rerun()
|
||||
>>>
|
||||
>>> if "vote" not in st.session_state:
|
||||
>>> st.write("Vote for your favorite")
|
||||
>>> if st.button("A"):
|
||||
>>> vote("A")
|
||||
>>> if st.button("B"):
|
||||
>>> vote("B")
|
||||
>>> else:
|
||||
>>> f"You voted for {st.session_state.vote['item']} because {st.session_state.vote['reason']}"
|
||||
|
||||
.. output::
|
||||
https://doc-modal-dialog.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
|
||||
func_or_title = title
|
||||
if isinstance(func_or_title, str):
|
||||
# Support passing the params via function decorator
|
||||
def wrapper(f: F) -> F:
|
||||
title: str = func_or_title
|
||||
return _dialog_decorator(non_optional_func=f, title=title, width=width)
|
||||
|
||||
return wrapper
|
||||
|
||||
func: F = func_or_title
|
||||
return _dialog_decorator(func, "", width=width)
|
||||
|
||||
|
||||
@overload
|
||||
def experimental_dialog_decorator(
|
||||
title: str, *, width: DialogWidth = "small"
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
# 'title' can be a function since `dialog_decorator` is a decorator. We just call it
|
||||
# 'title' here though to make the user-doc more friendly as we want the user to pass a
|
||||
# title, not a function. The user is supposed to call it like @st.dialog("my_title"),
|
||||
# which makes 'title' a positional arg, hence this 'trick'. The overload is required to
|
||||
# have a good type hint for the decorated function args.
|
||||
@overload
|
||||
def experimental_dialog_decorator(title: F, *, width: DialogWidth = "small") -> F: ...
|
||||
|
||||
|
||||
@gather_metrics("experimental_dialog")
|
||||
def experimental_dialog_decorator(
|
||||
title: F | str, *, width: DialogWidth = "small"
|
||||
) -> F | Callable[[F], F]:
|
||||
"""Deprecated alias for @st.dialog.
|
||||
See the docstring for the decorator's new name.
|
||||
"""
|
||||
func_or_title = title
|
||||
if isinstance(func_or_title, str):
|
||||
# Support passing the params via function decorator
|
||||
def wrapper(f: F) -> F:
|
||||
title: str = func_or_title
|
||||
return _dialog_decorator(
|
||||
non_optional_func=f,
|
||||
title=title,
|
||||
width=width,
|
||||
should_show_deprecation_warning=True,
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
func: F = func_or_title
|
||||
return _dialog_decorator(
|
||||
func, "", width=width, should_show_deprecation_warning=True
|
||||
)
|
||||
@@ -0,0 +1,558 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Allows us to create and absorb changes (aka Deltas) to elements."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import types
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import streamlit
|
||||
from streamlit.proto.DocString_pb2 import DocString as DocStringProto
|
||||
from streamlit.proto.DocString_pb2 import Member as MemberProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner.script_runner import (
|
||||
__file__ as SCRIPTRUNNER_FILENAME,
|
||||
)
|
||||
from streamlit.runtime.secrets import Secrets
|
||||
from streamlit.string_util import is_mem_address_str
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
CONFUSING_STREAMLIT_SIG_PREFIXES: Final = ("(element, ",)
|
||||
|
||||
|
||||
class HelpMixin:
|
||||
@gather_metrics("help")
|
||||
def help(self, obj: Any = streamlit) -> DeltaGenerator:
|
||||
"""Display help and other information for a given object.
|
||||
|
||||
Depending on the type of object that is passed in, this displays the
|
||||
object's name, type, value, signature, docstring, and member variables,
|
||||
methods — as well as the values/docstring of members and methods.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj : any
|
||||
The object whose information should be displayed. If left
|
||||
unspecified, this call will display help for Streamlit itself.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
Don't remember how to initialize a dataframe? Try this:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas
|
||||
>>>
|
||||
>>> st.help(pandas.DataFrame)
|
||||
|
||||
.. output::
|
||||
https://doc-string.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
Want to quickly check what data type is output by a certain function?
|
||||
Try:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> x = my_poorly_documented_function()
|
||||
>>> st.help(x)
|
||||
|
||||
Want to quickly inspect an object? No sweat:
|
||||
|
||||
>>> class Dog:
|
||||
>>> '''A typical dog.'''
|
||||
>>>
|
||||
>>> def __init__(self, breed, color):
|
||||
>>> self.breed = breed
|
||||
>>> self.color = color
|
||||
>>>
|
||||
>>> def bark(self):
|
||||
>>> return 'Woof!'
|
||||
>>>
|
||||
>>>
|
||||
>>> fido = Dog("poodle", "white")
|
||||
>>>
|
||||
>>> st.help(fido)
|
||||
|
||||
.. output::
|
||||
https://doc-string1.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
And if you're using Magic, you can get help for functions, classes,
|
||||
and modules without even typing ``st.help``:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas
|
||||
>>>
|
||||
>>> # Get help for Pandas read_csv:
|
||||
>>> pandas.read_csv
|
||||
>>>
|
||||
>>> # Get help for Streamlit itself:
|
||||
>>> st
|
||||
|
||||
.. output::
|
||||
https://doc-string2.streamlit.app/
|
||||
height: 700px
|
||||
"""
|
||||
doc_string_proto = DocStringProto()
|
||||
_marshall(doc_string_proto, obj)
|
||||
return self.dg._enqueue("doc_string", doc_string_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _marshall(doc_string_proto: DocStringProto, obj: Any) -> None:
|
||||
"""Construct a DocString object.
|
||||
|
||||
See DeltaGenerator.help for docs.
|
||||
"""
|
||||
var_name = _get_variable_name()
|
||||
if var_name is not None:
|
||||
doc_string_proto.name = var_name
|
||||
|
||||
obj_type = _get_type_as_str(obj)
|
||||
doc_string_proto.type = obj_type
|
||||
|
||||
obj_docs = _get_docstring(obj)
|
||||
if obj_docs is not None:
|
||||
doc_string_proto.doc_string = obj_docs
|
||||
|
||||
obj_value = _get_value(obj, var_name)
|
||||
if obj_value is not None:
|
||||
doc_string_proto.value = obj_value
|
||||
|
||||
doc_string_proto.members.extend(_get_members(obj))
|
||||
|
||||
|
||||
def _get_name(obj):
|
||||
# Try to get the fully-qualified name of the object.
|
||||
# For example:
|
||||
# st.help(bar.Baz(123))
|
||||
#
|
||||
# The name is bar.Baz
|
||||
name = getattr(obj, "__qualname__", None)
|
||||
if name:
|
||||
return name
|
||||
|
||||
# Try to get the name of the object.
|
||||
# For example:
|
||||
# st.help(bar.Baz(123))
|
||||
#
|
||||
# The name is Baz
|
||||
return getattr(obj, "__name__", None)
|
||||
|
||||
|
||||
def _get_module(obj):
|
||||
return getattr(obj, "__module__", None)
|
||||
|
||||
|
||||
def _get_signature(obj):
|
||||
if not inspect.isclass(obj) and not callable(obj):
|
||||
return None
|
||||
|
||||
sig = ""
|
||||
|
||||
# TODO: Can we replace below with this?
|
||||
# with contextlib.suppress(ValueError):
|
||||
# sig = str(inspect.signature(obj))
|
||||
|
||||
try:
|
||||
sig = str(inspect.signature(obj))
|
||||
except ValueError:
|
||||
sig = "(...)"
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
is_delta_gen = False
|
||||
with contextlib.suppress(AttributeError):
|
||||
is_delta_gen = obj.__module__ == "streamlit.delta_generator"
|
||||
# Functions such as numpy.minimum don't have a __module__ attribute,
|
||||
# since we're only using it to check if its a DeltaGenerator, its ok
|
||||
# to continue
|
||||
|
||||
if is_delta_gen:
|
||||
for prefix in CONFUSING_STREAMLIT_SIG_PREFIXES:
|
||||
if sig.startswith(prefix):
|
||||
sig = sig.replace(prefix, "(")
|
||||
break
|
||||
|
||||
return sig
|
||||
|
||||
|
||||
def _get_docstring(obj):
|
||||
doc_string = inspect.getdoc(obj)
|
||||
|
||||
# Sometimes an object has no docstring, but the object's type does.
|
||||
# If that's the case here, use the type's docstring.
|
||||
# For objects where type is "type" we do not print the docs (e.g. int).
|
||||
# We also do not print the docs for functions and methods if the docstring is empty.
|
||||
if doc_string is None:
|
||||
obj_type = type(obj)
|
||||
|
||||
if (
|
||||
obj_type is not type
|
||||
and obj_type is not types.ModuleType
|
||||
and not inspect.isfunction(obj)
|
||||
and not inspect.ismethod(obj)
|
||||
):
|
||||
doc_string = inspect.getdoc(obj_type)
|
||||
|
||||
if doc_string:
|
||||
return doc_string.strip()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_variable_name():
|
||||
"""Try to get the name of the variable in the current line, as set by the user.
|
||||
|
||||
For example:
|
||||
foo = bar.Baz(123)
|
||||
st.help(foo)
|
||||
|
||||
The name is "foo"
|
||||
"""
|
||||
code = _get_current_line_of_code_as_str()
|
||||
|
||||
if code is None:
|
||||
return None
|
||||
|
||||
return _get_variable_name_from_code_str(code)
|
||||
|
||||
|
||||
def _get_variable_name_from_code_str(code):
|
||||
tree = ast.parse(code)
|
||||
|
||||
# Example:
|
||||
#
|
||||
# tree = Module(
|
||||
# body=[
|
||||
# Expr(
|
||||
# value=Call(
|
||||
# args=[
|
||||
# Name(id='the variable name')
|
||||
# ],
|
||||
# keywords=[
|
||||
# ???
|
||||
# ],
|
||||
# )
|
||||
# )
|
||||
# ]
|
||||
# )
|
||||
|
||||
# Check if this is an magic call (i.e. it's not st.help or st.write).
|
||||
# If that's the case, just clean it up and return it.
|
||||
if not _is_stcommand(tree, command_name="help") and not _is_stcommand(
|
||||
tree, command_name="write"
|
||||
):
|
||||
# A common pattern is to add "," at the end of a magic command to make it print.
|
||||
# This removes that final ",", so it looks nicer.
|
||||
code = code.removesuffix(",")
|
||||
|
||||
return code
|
||||
|
||||
arg_node = _get_stcommand_arg(tree)
|
||||
|
||||
# If st.help() is called without an argument, return no variable name.
|
||||
if not arg_node:
|
||||
return None
|
||||
|
||||
# If walrus, get name.
|
||||
# E.g. st.help(foo := 123) should give you "foo".
|
||||
elif type(arg_node) is ast.NamedExpr:
|
||||
# This next "if" will always be true, but need to add this for the type-checking test to
|
||||
# pass.
|
||||
if type(arg_node.target) is ast.Name:
|
||||
return arg_node.target.id
|
||||
|
||||
# If constant, there's no variable name.
|
||||
# E.g. st.help("foo") or st.help(123) should give you None.
|
||||
elif type(arg_node) is ast.Constant:
|
||||
return None
|
||||
|
||||
# Otherwise, return whatever is inside st.help(<-- here -->)
|
||||
|
||||
# But, if multiline, only return the first line.
|
||||
code_lines = code.split("\n")
|
||||
is_multiline = len(code_lines) > 1
|
||||
|
||||
start_offset = arg_node.col_offset
|
||||
|
||||
if is_multiline:
|
||||
first_lineno = arg_node.lineno - 1 # Lines are 1-indexed!
|
||||
first_line = code_lines[first_lineno]
|
||||
end_offset = None
|
||||
|
||||
else:
|
||||
first_line = code_lines[0]
|
||||
end_offset = getattr(arg_node, "end_col_offset", -1)
|
||||
|
||||
return first_line[start_offset:end_offset]
|
||||
|
||||
|
||||
_NEWLINES = re.compile(r"[\n\r]+")
|
||||
|
||||
|
||||
def _get_current_line_of_code_as_str():
|
||||
scriptrunner_frame = _get_scriptrunner_frame()
|
||||
|
||||
if scriptrunner_frame is None:
|
||||
# If there's no ScriptRunner frame, something weird is going on. This
|
||||
# can happen when the script is executed with `python myscript.py`.
|
||||
# Either way, let's bail out nicely just in case there's some valid
|
||||
# edge case where this is OK.
|
||||
return None
|
||||
|
||||
code_context = scriptrunner_frame.code_context
|
||||
|
||||
if not code_context:
|
||||
# Sometimes a frame has no code_context. This can happen inside certain exec() calls, for
|
||||
# example. If this happens, we can't determine the variable name. Just return.
|
||||
# For the background on why exec() doesn't produce code_context, see
|
||||
# https://stackoverflow.com/a/12072941
|
||||
return None
|
||||
|
||||
code_as_string = "".join(code_context)
|
||||
return re.sub(_NEWLINES, "", code_as_string.strip())
|
||||
|
||||
|
||||
def _get_scriptrunner_frame():
|
||||
prev_frame = None
|
||||
scriptrunner_frame = None
|
||||
|
||||
# Look back in call stack to get the variable name passed into st.help().
|
||||
# The frame *before* the ScriptRunner frame is the correct one.
|
||||
# IMPORTANT: This will change if we refactor the code. But hopefully our tests will catch the
|
||||
# issue and we'll fix it before it lands upstream!
|
||||
for frame in inspect.stack():
|
||||
# Check if this is running inside a funny "exec()" block that won't provide the info we
|
||||
# need. If so, just quit.
|
||||
if frame.code_context is None:
|
||||
return None
|
||||
|
||||
if frame.filename == SCRIPTRUNNER_FILENAME:
|
||||
scriptrunner_frame = prev_frame
|
||||
break
|
||||
|
||||
prev_frame = frame
|
||||
|
||||
return scriptrunner_frame
|
||||
|
||||
|
||||
def _is_stcommand(tree, command_name):
|
||||
"""Checks whether the AST in tree is a call for command_name."""
|
||||
root_node = tree.body[0].value
|
||||
|
||||
if not isinstance(root_node, ast.Call):
|
||||
return False
|
||||
|
||||
return (
|
||||
# st call called without module. E.g. "help()"
|
||||
getattr(root_node.func, "id", None) == command_name
|
||||
or
|
||||
# st call called with module. E.g. "foo.help()" (where usually "foo" is "st")
|
||||
getattr(root_node.func, "attr", None) == command_name
|
||||
)
|
||||
|
||||
|
||||
def _get_stcommand_arg(tree):
|
||||
"""Gets the argument node for the st command in tree (AST)."""
|
||||
|
||||
root_node = tree.body[0].value
|
||||
|
||||
if root_node.args:
|
||||
return root_node.args[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_type_as_str(obj):
|
||||
if inspect.isclass(obj):
|
||||
return "class"
|
||||
|
||||
return str(type(obj).__name__)
|
||||
|
||||
|
||||
def _get_first_line(text):
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
left, _, _ = text.partition("\n")
|
||||
return left
|
||||
|
||||
|
||||
def _get_weight(value):
|
||||
if inspect.ismodule(value):
|
||||
return 3
|
||||
if inspect.isclass(value):
|
||||
return 2
|
||||
if callable(value):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _get_value(obj, var_name):
|
||||
obj_value = _get_human_readable_value(obj)
|
||||
|
||||
if obj_value is not None:
|
||||
return obj_value
|
||||
|
||||
# If there's no human-readable value, it's some complex object.
|
||||
# So let's provide other info about it.
|
||||
name = _get_name(obj)
|
||||
|
||||
if name:
|
||||
name_obj = obj
|
||||
else:
|
||||
# If the object itself doesn't have a name, then it's probably an instance
|
||||
# of some class Foo. So let's show info about Foo in the value slot.
|
||||
name_obj = type(obj)
|
||||
name = _get_name(name_obj)
|
||||
|
||||
module = _get_module(name_obj)
|
||||
sig = _get_signature(name_obj) or ""
|
||||
|
||||
if name:
|
||||
if module:
|
||||
obj_value = f"{module}.{name}{sig}"
|
||||
else:
|
||||
obj_value = f"{name}{sig}"
|
||||
|
||||
if obj_value == var_name:
|
||||
# No need to repeat the same info.
|
||||
# For example: st.help(re) shouldn't show "re module re", just "re module".
|
||||
obj_value = None
|
||||
|
||||
return obj_value
|
||||
|
||||
|
||||
def _get_human_readable_value(value):
|
||||
if isinstance(value, Secrets):
|
||||
# Don't want to read secrets.toml because that will show a warning if there's no
|
||||
# secrets.toml file.
|
||||
return None
|
||||
|
||||
if inspect.isclass(value) or inspect.ismodule(value) or callable(value):
|
||||
return None
|
||||
|
||||
value_str = repr(value)
|
||||
|
||||
if isinstance(value, str):
|
||||
# Special-case strings as human-readable because they're allowed to look like
|
||||
# "<foo blarg at 0x15ee6f9a0>".
|
||||
return _shorten(value_str)
|
||||
|
||||
if is_mem_address_str(value_str):
|
||||
# If value_str looks like "<foo blarg at 0x15ee6f9a0>" it's not human readable.
|
||||
return None
|
||||
|
||||
return _shorten(value_str)
|
||||
|
||||
|
||||
def _shorten(s, length=300):
|
||||
s = s.strip()
|
||||
return s[:length] + "..." if len(s) > length else s
|
||||
|
||||
|
||||
def _is_computed_property(obj, attr_name):
|
||||
obj_class = getattr(obj, "__class__", None)
|
||||
|
||||
if not obj_class:
|
||||
return False
|
||||
|
||||
# Go through superclasses in order of inheritance (mro) to see if any of them have an
|
||||
# attribute called attr_name. If so, check if it's a @property.
|
||||
for parent_class in inspect.getmro(obj_class):
|
||||
class_attr = getattr(parent_class, attr_name, None)
|
||||
|
||||
if class_attr is None:
|
||||
continue
|
||||
|
||||
# If is property, return it.
|
||||
if isinstance(class_attr, property) or inspect.isgetsetdescriptor(class_attr):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _get_members(obj):
|
||||
members_for_sorting = []
|
||||
|
||||
for attr_name in dir(obj):
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
|
||||
try:
|
||||
is_computed_value = _is_computed_property(obj, attr_name)
|
||||
if is_computed_value:
|
||||
parent_attr = getattr(obj.__class__, attr_name)
|
||||
|
||||
member_type = "property"
|
||||
|
||||
weight = 0
|
||||
member_docs = _get_docstring(parent_attr)
|
||||
member_value = None
|
||||
else:
|
||||
attr_value = getattr(obj, attr_name)
|
||||
weight = _get_weight(attr_value)
|
||||
|
||||
human_readable_value = _get_human_readable_value(attr_value)
|
||||
|
||||
member_type = _get_type_as_str(attr_value)
|
||||
|
||||
if human_readable_value is None:
|
||||
member_docs = _get_docstring(attr_value)
|
||||
member_value = None
|
||||
else:
|
||||
member_docs = None
|
||||
member_value = human_readable_value
|
||||
except AttributeError:
|
||||
# If there's an AttributeError, we can just skip it.
|
||||
# This can happen when members are exposed with `dir()`
|
||||
# but are conditionally unavailable.
|
||||
continue
|
||||
|
||||
if member_type == "module":
|
||||
# Don't pollute the output with all imported modules.
|
||||
continue
|
||||
|
||||
member = MemberProto()
|
||||
member.name = attr_name
|
||||
member.type = member_type
|
||||
|
||||
if member_docs is not None:
|
||||
member.doc_string = _get_first_line(member_docs)
|
||||
|
||||
if member_value is not None:
|
||||
member.value = member_value
|
||||
|
||||
members_for_sorting.append((weight, member))
|
||||
|
||||
if members_for_sorting:
|
||||
sorted_members = sorted(members_for_sorting, key=lambda x: (x[0], x[1].name))
|
||||
return [m for _, m in sorted_members]
|
||||
|
||||
return []
|
||||
130
myenv/lib/python3.11/site-packages/streamlit/elements/empty.py
Normal file
130
myenv/lib/python3.11/site-packages/streamlit/elements/empty.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Empty_pb2 import Empty as EmptyProto
|
||||
from streamlit.proto.Skeleton_pb2 import Skeleton as SkeletonProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class EmptyMixin:
|
||||
@gather_metrics("empty")
|
||||
def empty(self) -> DeltaGenerator:
|
||||
"""Insert a single-element container.
|
||||
|
||||
Inserts a container into your app that can be used to hold a single element.
|
||||
This allows you to, for example, remove elements at any point, or replace
|
||||
several elements at once (using a child multi-element container).
|
||||
|
||||
To insert/replace/clear an element on the returned container, you can
|
||||
use ``with`` notation or just call methods directly on the returned object.
|
||||
See examples below.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Inside a ``with st.empty():`` block, each displayed element will
|
||||
replace the previous one.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> with st.empty():
|
||||
... for seconds in range(10):
|
||||
... st.write(f"⏳ {seconds} seconds have passed")
|
||||
... time.sleep(1)
|
||||
... st.write(":material/check: 10 seconds over!")
|
||||
... st.button("Rerun")
|
||||
|
||||
.. output::
|
||||
https://doc-empty.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
You can use an ``st.empty`` to replace multiple elements in
|
||||
succession. Use ``st.container`` inside ``st.empty`` to display (and
|
||||
later replace) a group of elements.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> st.button("Start over")
|
||||
>>>
|
||||
>>> placeholder = st.empty()
|
||||
>>> placeholder.markdown("Hello")
|
||||
>>> time.sleep(1)
|
||||
>>>
|
||||
>>> placeholder.progress(0, "Wait for it...")
|
||||
>>> time.sleep(1)
|
||||
>>> placeholder.progress(50, "Wait for it...")
|
||||
>>> time.sleep(1)
|
||||
>>> placeholder.progress(100, "Wait for it...")
|
||||
>>> time.sleep(1)
|
||||
>>>
|
||||
>>> with placeholder.container():
|
||||
... st.line_chart({"data": [1, 5, 2, 6]})
|
||||
... time.sleep(1)
|
||||
... st.markdown("3...")
|
||||
... time.sleep(1)
|
||||
... st.markdown("2...")
|
||||
... time.sleep(1)
|
||||
... st.markdown("1...")
|
||||
... time.sleep(1)
|
||||
>>>
|
||||
>>> placeholder.markdown("Poof!")
|
||||
>>> time.sleep(1)
|
||||
>>>
|
||||
>>> placeholder.empty()
|
||||
|
||||
.. output::
|
||||
https://doc-empty-placeholder.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
empty_proto = EmptyProto()
|
||||
return self.dg._enqueue("empty", empty_proto)
|
||||
|
||||
@gather_metrics("_skeleton")
|
||||
def _skeleton(self, *, height: int | None = None) -> DeltaGenerator:
|
||||
"""Insert a single-element container which displays a "skeleton" placeholder.
|
||||
|
||||
Inserts a container into your app that can be used to hold a single element.
|
||||
This allows you to, for example, remove elements at any point, or replace
|
||||
several elements at once (using a child multi-element container).
|
||||
|
||||
To insert/replace/clear an element on the returned container, you can
|
||||
use ``with`` notation or just call methods directly on the returned object.
|
||||
See some of the examples below.
|
||||
|
||||
This is an internal method and should not be used directly.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
height: int or None
|
||||
Desired height of the skeleton expressed in pixels. If None, a
|
||||
default height is used.
|
||||
"""
|
||||
skeleton_proto = SkeletonProto()
|
||||
if height:
|
||||
skeleton_proto.height = height
|
||||
return self.dg._enqueue("skeleton", skeleton_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,341 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING, Callable, Final, TypeVar, cast
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.errors import (
|
||||
MarkdownFormattedException,
|
||||
StreamlitAPIWarning,
|
||||
)
|
||||
from streamlit.logger import get_logger
|
||||
from streamlit.proto.Exception_pb2 import Exception as ExceptionProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
_LOGGER: Final = get_logger(__name__)
|
||||
|
||||
# When client.showErrorDetails is False, we show a generic warning in the
|
||||
# frontend when we encounter an uncaught app exception.
|
||||
_GENERIC_UNCAUGHT_EXCEPTION_TEXT: Final = "This app has encountered an error. The original error message is redacted to prevent data leaks. Full error details have been recorded in the logs (if you're on Streamlit Cloud, click on 'Manage app' in the lower right of your app)."
|
||||
|
||||
|
||||
class ExceptionMixin:
|
||||
@gather_metrics("exception")
|
||||
def exception(self, exception: BaseException) -> DeltaGenerator:
|
||||
"""Display an exception.
|
||||
|
||||
In the lower-right corner of the exception, Streamlit displays links to
|
||||
Google and ChatGPT that are prefilled with the contents of the
|
||||
exception message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exception : Exception
|
||||
The exception to display.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> e = RuntimeError("This is an exception of type RuntimeError")
|
||||
>>> st.exception(e)
|
||||
|
||||
.. output ::
|
||||
https://doc-status-exception.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
return _exception(self.dg, exception)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
# TODO(lawilby): confirm whether we want to track metrics here with lukasmasuch.
|
||||
@gather_metrics("exception")
|
||||
def _exception(
|
||||
dg: DeltaGenerator,
|
||||
exception: BaseException,
|
||||
is_uncaught_app_exception: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
exception_proto = ExceptionProto()
|
||||
marshall(exception_proto, exception, is_uncaught_app_exception)
|
||||
return dg._enqueue("exception", exception_proto)
|
||||
|
||||
|
||||
def marshall(
|
||||
exception_proto: ExceptionProto,
|
||||
exception: BaseException,
|
||||
is_uncaught_app_exception: bool = False,
|
||||
) -> None:
|
||||
"""Marshalls an Exception.proto message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exception_proto : Exception.proto
|
||||
The Exception protobuf to fill out.
|
||||
|
||||
exception : BaseException
|
||||
The exception whose data we're extracting.
|
||||
|
||||
is_uncaught_app_exception: bool
|
||||
The exception originates from an uncaught error during script execution.
|
||||
"""
|
||||
is_markdown_exception = isinstance(exception, MarkdownFormattedException)
|
||||
|
||||
# Some exceptions (like UserHashError) have an alternate_name attribute so
|
||||
# we can pretend to the user that the exception is called something else.
|
||||
if getattr(exception, "alternate_name", None) is not None:
|
||||
exception_proto.type = exception.alternate_name # type: ignore[attr-defined]
|
||||
else:
|
||||
exception_proto.type = type(exception).__name__
|
||||
|
||||
stack_trace = _get_stack_trace_str_list(exception)
|
||||
|
||||
exception_proto.stack_trace.extend(stack_trace)
|
||||
exception_proto.is_warning = isinstance(exception, Warning)
|
||||
|
||||
try:
|
||||
if isinstance(exception, SyntaxError):
|
||||
# SyntaxErrors have additional fields (filename, text, lineno,
|
||||
# offset) that we can use for a nicely-formatted message telling
|
||||
# the user what to fix.
|
||||
exception_proto.message = _format_syntax_error_message(exception)
|
||||
else:
|
||||
exception_proto.message = str(exception).strip()
|
||||
exception_proto.message_is_markdown = is_markdown_exception
|
||||
|
||||
except Exception as str_exception:
|
||||
# Sometimes the exception's __str__/__unicode__ method itself
|
||||
# raises an error.
|
||||
exception_proto.message = ""
|
||||
_LOGGER.warning(
|
||||
"""
|
||||
|
||||
Streamlit was unable to parse the data from an exception in the user's script.
|
||||
This is usually due to a bug in the Exception object itself. Here is some info
|
||||
about that Exception object, so you can report a bug to the original author:
|
||||
|
||||
Exception type:
|
||||
%s
|
||||
|
||||
Problem:
|
||||
%s
|
||||
|
||||
Traceback:
|
||||
%s
|
||||
|
||||
""",
|
||||
type(exception).__name__,
|
||||
str_exception,
|
||||
"\n".join(_get_stack_trace_str_list(str_exception)),
|
||||
)
|
||||
|
||||
if is_uncaught_app_exception:
|
||||
show_error_details = config.get_option("client.showErrorDetails")
|
||||
|
||||
show_message = (
|
||||
show_error_details == config.ShowErrorDetailsConfigOptions.FULL
|
||||
or config.ShowErrorDetailsConfigOptions.is_true_variation(
|
||||
show_error_details
|
||||
)
|
||||
)
|
||||
# False is a legacy config option still in-use in community cloud. It is equivalent
|
||||
# to "stacktrace".
|
||||
show_trace = (
|
||||
show_message
|
||||
or show_error_details == config.ShowErrorDetailsConfigOptions.STACKTRACE
|
||||
or config.ShowErrorDetailsConfigOptions.is_false_variation(
|
||||
show_error_details
|
||||
)
|
||||
)
|
||||
show_type = (
|
||||
show_trace
|
||||
or show_error_details == config.ShowErrorDetailsConfigOptions.TYPE
|
||||
)
|
||||
|
||||
if not show_message:
|
||||
exception_proto.message = _GENERIC_UNCAUGHT_EXCEPTION_TEXT
|
||||
if not show_type:
|
||||
exception_proto.ClearField("type")
|
||||
else:
|
||||
type_str = str(type(exception))
|
||||
exception_proto.type = type_str.replace("<class '", "").replace("'>", "")
|
||||
if not show_trace:
|
||||
exception_proto.ClearField("stack_trace")
|
||||
|
||||
|
||||
def _format_syntax_error_message(exception: SyntaxError) -> str:
|
||||
"""Returns a nicely formatted SyntaxError message that emulates
|
||||
what the Python interpreter outputs.
|
||||
|
||||
For example:
|
||||
|
||||
> File "raven.py", line 3
|
||||
> st.write('Hello world!!'))
|
||||
> ^
|
||||
> SyntaxError: invalid syntax
|
||||
|
||||
"""
|
||||
if exception.text:
|
||||
if exception.offset is not None:
|
||||
caret_indent = " " * max(exception.offset - 1, 0)
|
||||
else:
|
||||
caret_indent = ""
|
||||
|
||||
return (
|
||||
'File "%(filename)s", line %(lineno)s\n'
|
||||
" %(text)s\n"
|
||||
" %(caret_indent)s^\n"
|
||||
"%(errname)s: %(msg)s"
|
||||
% {
|
||||
"filename": exception.filename,
|
||||
"lineno": exception.lineno,
|
||||
"text": exception.text.rstrip(),
|
||||
"caret_indent": caret_indent,
|
||||
"errname": type(exception).__name__,
|
||||
"msg": exception.msg,
|
||||
}
|
||||
)
|
||||
# If a few edge cases, SyntaxErrors don't have all these nice fields. So we
|
||||
# have a fall back here.
|
||||
# Example edge case error message: encoding declaration in Unicode string
|
||||
return str(exception)
|
||||
|
||||
|
||||
def _get_stack_trace_str_list(exception: BaseException) -> list[str]:
|
||||
"""Get the stack trace for the given exception.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exception : BaseException
|
||||
The exception to extract the traceback from
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple of two string lists
|
||||
The exception traceback as two lists of strings. The first represents the part
|
||||
of the stack trace the users don't typically want to see, containing internal
|
||||
Streamlit code. The second is whatever comes after the Streamlit stack trace,
|
||||
which is usually what the user wants.
|
||||
|
||||
"""
|
||||
extracted_traceback: traceback.StackSummary | None = None
|
||||
if isinstance(exception, StreamlitAPIWarning):
|
||||
extracted_traceback = exception.tacked_on_stack
|
||||
elif hasattr(exception, "__traceback__"):
|
||||
extracted_traceback = traceback.extract_tb(exception.__traceback__)
|
||||
|
||||
# Format the extracted traceback and add it to the protobuf element.
|
||||
if extracted_traceback is None:
|
||||
trace_str_list = [
|
||||
"Cannot extract the stack trace for this exception. "
|
||||
"Try calling exception() within the `catch` block."
|
||||
]
|
||||
else:
|
||||
internal_frames, external_frames = _split_internal_streamlit_frames(
|
||||
extracted_traceback
|
||||
)
|
||||
|
||||
if external_frames:
|
||||
trace_str_list = traceback.format_list(external_frames)
|
||||
else:
|
||||
trace_str_list = traceback.format_list(internal_frames)
|
||||
|
||||
trace_str_list = [item.strip() for item in trace_str_list]
|
||||
|
||||
return trace_str_list
|
||||
|
||||
|
||||
def _is_in_package(file: str, package_path: str) -> bool:
|
||||
"""True if the given file is part of package_path."""
|
||||
try:
|
||||
common_prefix = os.path.commonprefix([os.path.realpath(file), package_path])
|
||||
except ValueError:
|
||||
# Raised if paths are on different drives.
|
||||
return False
|
||||
|
||||
return common_prefix == package_path
|
||||
|
||||
|
||||
def _split_internal_streamlit_frames(
|
||||
extracted_tb: traceback.StackSummary,
|
||||
) -> tuple[list[traceback.FrameSummary], list[traceback.FrameSummary]]:
|
||||
"""Split the traceback into a Streamlit-internal part and an external part.
|
||||
|
||||
The internal part is everything up to (but excluding) the first frame belonging to
|
||||
the user's code. The external part is everything else.
|
||||
|
||||
So if the stack looks like this:
|
||||
|
||||
1. Streamlit frame
|
||||
2. Pandas frame
|
||||
3. Altair frame
|
||||
4. Streamlit frame
|
||||
5. User frame
|
||||
6. User frame
|
||||
7. Streamlit frame
|
||||
8. Matplotlib frame
|
||||
|
||||
...then this should return 1-4 as the internal traceback and 5-8 as the external.
|
||||
|
||||
(Note that something like the example above is extremely unlikely to happen since
|
||||
it's not like Altair is calling Streamlit code, but you get the idea.)
|
||||
"""
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
if not ctx:
|
||||
return [], list(extracted_tb)
|
||||
|
||||
package_path = os.path.join(os.path.realpath(str(ctx.main_script_parent)), "")
|
||||
|
||||
return _split_list(
|
||||
extracted_tb,
|
||||
split_point=lambda tb: _is_in_package(tb.filename, package_path),
|
||||
)
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _split_list(
|
||||
orig_list: list[T], split_point: Callable[[T], bool]
|
||||
) -> tuple[list[T], list[T]]:
|
||||
before: list[T] = []
|
||||
after: list[T] = []
|
||||
|
||||
saw_split_point = False
|
||||
|
||||
for item in orig_list:
|
||||
if not saw_split_point:
|
||||
if split_point(item):
|
||||
saw_split_point = True
|
||||
|
||||
if saw_split_point:
|
||||
after.append(item)
|
||||
else:
|
||||
before.append(item)
|
||||
|
||||
return before, after
|
||||
354
myenv/lib/python3.11/site-packages/streamlit/elements/form.py
Normal file
354
myenv/lib/python3.11/site-packages/streamlit/elements/form.py
Normal file
@@ -0,0 +1,354 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from streamlit.elements.lib.form_utils import FormData, current_form_id, is_in_form
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_cache_replay_rules,
|
||||
check_session_state_rules,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto import Block_pb2
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.runtime.state import WidgetArgs, WidgetCallback, WidgetKwargs
|
||||
|
||||
|
||||
def _build_duplicate_form_message(user_key: str | None = None) -> str:
|
||||
if user_key is not None:
|
||||
message = textwrap.dedent(
|
||||
f"""
|
||||
There are multiple identical forms with `key='{user_key}'`.
|
||||
|
||||
To fix this, please make sure that the `key` argument is unique for
|
||||
each `st.form` you create.
|
||||
"""
|
||||
)
|
||||
else:
|
||||
message = textwrap.dedent(
|
||||
"""
|
||||
There are multiple identical forms with the same generated key.
|
||||
|
||||
When a form is created, it's assigned an internal key based on
|
||||
its structure. Multiple forms with an identical structure will
|
||||
result in the same internal key, which causes this error.
|
||||
|
||||
To fix this error, please pass a unique `key` argument to
|
||||
`st.form`.
|
||||
"""
|
||||
)
|
||||
|
||||
return message.strip("\n")
|
||||
|
||||
|
||||
class FormMixin:
|
||||
@gather_metrics("form")
|
||||
def form(
|
||||
self,
|
||||
key: str,
|
||||
clear_on_submit: bool = False,
|
||||
*,
|
||||
enter_to_submit: bool = True,
|
||||
border: bool = True,
|
||||
) -> DeltaGenerator:
|
||||
"""Create a form that batches elements together with a "Submit" button.
|
||||
|
||||
A form is a container that visually groups other elements and
|
||||
widgets together, and contains a Submit button. When the form's
|
||||
Submit button is pressed, all widget values inside the form will be
|
||||
sent to Streamlit in a batch.
|
||||
|
||||
To add elements to a form object, you can use ``with`` notation
|
||||
(preferred) or just call methods directly on the form. See
|
||||
examples below.
|
||||
|
||||
Forms have a few constraints:
|
||||
|
||||
- Every form must contain a ``st.form_submit_button``.
|
||||
- ``st.button`` and ``st.download_button`` cannot be added to a form.
|
||||
- Forms can appear anywhere in your app (sidebar, columns, etc),
|
||||
but they cannot be embedded inside other forms.
|
||||
- Within a form, the only widget that can have a callback function is
|
||||
``st.form_submit_button``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
key : str
|
||||
A string that identifies the form. Each form must have its own
|
||||
key. (This key is not displayed to the user in the interface.)
|
||||
clear_on_submit : bool
|
||||
If True, all widgets inside the form will be reset to their default
|
||||
values after the user presses the Submit button. Defaults to False.
|
||||
(Note that Custom Components are unaffected by this flag, and
|
||||
will not be reset to their defaults on form submission.)
|
||||
enter_to_submit : bool
|
||||
Whether to submit the form when a user presses Enter while
|
||||
interacting with a widget inside the form.
|
||||
|
||||
If this is ``True`` (default), pressing Enter while interacting
|
||||
with a form widget is equivalent to clicking the first
|
||||
``st.form_submit_button`` in the form.
|
||||
|
||||
If this is ``False``, the user must click an
|
||||
``st.form_submit_button`` to submit the form.
|
||||
|
||||
If the first ``st.form_submit_button`` in the form is disabled,
|
||||
the form will override submission behavior with
|
||||
``enter_to_submit=False``.
|
||||
|
||||
border : bool
|
||||
Whether to show a border around the form. Defaults to True.
|
||||
|
||||
.. note::
|
||||
Not showing a border can be confusing to viewers since interacting with a
|
||||
widget in the form will do nothing. You should only remove the border if
|
||||
there's another border (e.g. because of an expander) or the form is small
|
||||
(e.g. just a text input and a submit button).
|
||||
|
||||
Examples
|
||||
--------
|
||||
Inserting elements using ``with`` notation:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.form("my_form"):
|
||||
... st.write("Inside the form")
|
||||
... slider_val = st.slider("Form slider")
|
||||
... checkbox_val = st.checkbox("Form checkbox")
|
||||
...
|
||||
... # Every form must have a submit button.
|
||||
... submitted = st.form_submit_button("Submit")
|
||||
... if submitted:
|
||||
... st.write("slider", slider_val, "checkbox", checkbox_val)
|
||||
>>> st.write("Outside the form")
|
||||
|
||||
.. output::
|
||||
https://doc-form1.streamlit.app/
|
||||
height: 425px
|
||||
|
||||
Inserting elements out of order:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> form = st.form("my_form")
|
||||
>>> form.slider("Inside the form")
|
||||
>>> st.slider("Outside the form")
|
||||
>>>
|
||||
>>> # Now add a submit button to the form:
|
||||
>>> form.form_submit_button("Submit")
|
||||
|
||||
.. output::
|
||||
https://doc-form2.streamlit.app/
|
||||
height: 375px
|
||||
|
||||
"""
|
||||
if is_in_form(self.dg):
|
||||
raise StreamlitAPIException("Forms cannot be nested in other forms.")
|
||||
|
||||
check_cache_replay_rules()
|
||||
check_session_state_rules(default_value=None, key=key, writes_allowed=False)
|
||||
|
||||
# A form is uniquely identified by its key.
|
||||
form_id = key
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is not None:
|
||||
new_form_id = form_id not in ctx.form_ids_this_run
|
||||
if new_form_id:
|
||||
ctx.form_ids_this_run.add(form_id)
|
||||
else:
|
||||
raise StreamlitAPIException(_build_duplicate_form_message(key))
|
||||
|
||||
block_proto = Block_pb2.Block()
|
||||
block_proto.form.form_id = form_id
|
||||
block_proto.form.clear_on_submit = clear_on_submit
|
||||
block_proto.form.enter_to_submit = enter_to_submit
|
||||
block_proto.form.border = border
|
||||
block_dg = self.dg._block(block_proto)
|
||||
|
||||
# Attach the form's button info to the newly-created block's
|
||||
# DeltaGenerator.
|
||||
block_dg._form_data = FormData(form_id)
|
||||
return block_dg
|
||||
|
||||
@gather_metrics("form_submit_button")
|
||||
def form_submit_button(
|
||||
self,
|
||||
label: str = "Submit",
|
||||
help: str | None = None,
|
||||
on_click: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
type: Literal["primary", "secondary", "tertiary"] = "secondary",
|
||||
icon: str | None = None,
|
||||
disabled: bool = False,
|
||||
use_container_width: bool = False,
|
||||
) -> bool:
|
||||
r"""Display a form submit button.
|
||||
|
||||
When this button is clicked, all widget values inside the form will be
|
||||
sent from the user's browser to your Streamlit server in a batch.
|
||||
|
||||
Every form must have at least one ``st.form_submit_button``. An
|
||||
``st.form_submit_button`` cannot exist outside of a form.
|
||||
|
||||
For more information about forms, check out our `docs
|
||||
<https://docs.streamlit.io/develop/concepts/architecture/forms>`_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this button is for. This
|
||||
defaults to ``"Submit"``. The label can optionally contain
|
||||
GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code, Links, and Images. Images display like
|
||||
icons, with a max height equal to the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
help : str or None
|
||||
A tooltip that gets displayed when the button is hovered over. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
on_click : callable
|
||||
An optional callback invoked when this button is clicked.
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
type : "primary", "secondary", or "tertiary"
|
||||
An optional string that specifies the button type. This can be one
|
||||
of the following:
|
||||
|
||||
- ``"primary"``: The button's background is the app's primary color
|
||||
for additional emphasis.
|
||||
- ``"secondary"`` (default): The button's background coordinates
|
||||
with the app's background color for normal emphasis.
|
||||
- ``"tertiary"``: The button is plain text without a border or
|
||||
background for subtly.
|
||||
|
||||
icon : str or None
|
||||
An optional emoji or icon to display next to the button label. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
disabled : bool
|
||||
Whether to disable the button. If this is ``False`` (default), the
|
||||
user can interact with the button. If this is ``True``, the button
|
||||
is grayed-out and can't be clicked.
|
||||
|
||||
If the first ``st.form_submit_button`` in the form is disabled,
|
||||
the form will override submission behavior with
|
||||
``enter_to_submit=False``.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to expand the button's width to fill its parent container.
|
||||
If ``use_container_width`` is ``False`` (default), Streamlit sizes
|
||||
the button to fit its contents. If ``use_container_width`` is
|
||||
``True``, the width of the button matches its parent container.
|
||||
|
||||
In both cases, if the contents of the button are wider than the
|
||||
parent container, the contents will line wrap.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the button was clicked.
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
# Checks whether the entered button type is one of the allowed options
|
||||
if type not in ["primary", "secondary", "tertiary"]:
|
||||
raise StreamlitAPIException(
|
||||
'The type argument to st.form_submit_button must be "primary", "secondary", or "tertiary". \n'
|
||||
f'The argument passed was "{type}".'
|
||||
)
|
||||
|
||||
return self._form_submit_button(
|
||||
label=label,
|
||||
help=help,
|
||||
on_click=on_click,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
type=type,
|
||||
icon=icon,
|
||||
disabled=disabled,
|
||||
use_container_width=use_container_width,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _form_submit_button(
|
||||
self,
|
||||
label: str = "Submit",
|
||||
help: str | None = None,
|
||||
on_click: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
type: Literal["primary", "secondary", "tertiary"] = "secondary",
|
||||
icon: str | None = None,
|
||||
disabled: bool = False,
|
||||
use_container_width: bool = False,
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> bool:
|
||||
form_id = current_form_id(self.dg)
|
||||
submit_button_key = f"FormSubmitter:{form_id}-{label}"
|
||||
return self.dg._button(
|
||||
label=label,
|
||||
key=submit_button_key,
|
||||
help=help,
|
||||
is_form_submitter=True,
|
||||
on_click=on_click,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
type=type,
|
||||
icon=icon,
|
||||
disabled=disabled,
|
||||
use_container_width=use_container_width,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,150 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Streamlit support for GraphViz charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import type_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.GraphVizChart_pb2 import GraphVizChart as GraphVizChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import graphviz
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
FigureOrDot: TypeAlias = Union[
|
||||
"graphviz.Graph", "graphviz.Digraph", "graphviz.Source", str
|
||||
]
|
||||
|
||||
|
||||
class GraphvizMixin:
|
||||
@gather_metrics("graphviz_chart")
|
||||
def graphviz_chart(
|
||||
self,
|
||||
figure_or_dot: FigureOrDot,
|
||||
use_container_width: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a graph using the dagre-d3 library.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure_or_dot : graphviz.dot.Graph, graphviz.dot.Digraph, graphviz.sources.Source, str
|
||||
The Graphlib graph object or dot string to display
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``False``
|
||||
(default), Streamlit sets the width of the chart to fit its contents
|
||||
according to the plotting library, up to the width of the parent
|
||||
container. If ``use_container_width`` is ``True``, Streamlit sets
|
||||
the width of the figure to match the width of the parent container.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import graphviz
|
||||
>>>
|
||||
>>> # Create a graphlib graph object
|
||||
>>> graph = graphviz.Digraph()
|
||||
>>> graph.edge("run", "intr")
|
||||
>>> graph.edge("intr", "runbl")
|
||||
>>> graph.edge("runbl", "run")
|
||||
>>> graph.edge("run", "kernel")
|
||||
>>> graph.edge("kernel", "zombie")
|
||||
>>> graph.edge("kernel", "sleep")
|
||||
>>> graph.edge("kernel", "runmem")
|
||||
>>> graph.edge("sleep", "swap")
|
||||
>>> graph.edge("swap", "runswap")
|
||||
>>> graph.edge("runswap", "new")
|
||||
>>> graph.edge("runswap", "runmem")
|
||||
>>> graph.edge("new", "runmem")
|
||||
>>> graph.edge("sleep", "runmem")
|
||||
>>>
|
||||
>>> st.graphviz_chart(graph)
|
||||
|
||||
Or you can render the chart from the graph using GraphViz's Dot
|
||||
language:
|
||||
|
||||
>>> st.graphviz_chart('''
|
||||
digraph {
|
||||
run -> intr
|
||||
intr -> runbl
|
||||
runbl -> run
|
||||
run -> kernel
|
||||
kernel -> zombie
|
||||
kernel -> sleep
|
||||
kernel -> runmem
|
||||
sleep -> swap
|
||||
swap -> runswap
|
||||
runswap -> new
|
||||
runswap -> runmem
|
||||
new -> runmem
|
||||
sleep -> runmem
|
||||
}
|
||||
''')
|
||||
|
||||
.. output::
|
||||
https://doc-graphviz-chart.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
# Generate element ID from delta path
|
||||
delta_path = self.dg._get_delta_path_str()
|
||||
element_id = calc_md5(delta_path.encode())
|
||||
|
||||
graphviz_chart_proto = GraphVizChartProto()
|
||||
|
||||
marshall(graphviz_chart_proto, figure_or_dot, use_container_width, element_id)
|
||||
return self.dg._enqueue("graphviz_chart", graphviz_chart_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: GraphVizChartProto,
|
||||
figure_or_dot: FigureOrDot,
|
||||
use_container_width: bool,
|
||||
element_id: str,
|
||||
) -> None:
|
||||
"""Construct a GraphViz chart object.
|
||||
|
||||
See DeltaGenerator.graphviz_chart for docs.
|
||||
"""
|
||||
|
||||
if type_util.is_graphviz_chart(figure_or_dot):
|
||||
dot = figure_or_dot.source
|
||||
engine = figure_or_dot.engine
|
||||
elif isinstance(figure_or_dot, str):
|
||||
dot = figure_or_dot
|
||||
engine = "dot"
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Unhandled type for graphviz chart: %s" % type(figure_or_dot)
|
||||
)
|
||||
|
||||
proto.spec = dot
|
||||
proto.engine = engine
|
||||
proto.use_container_width = use_container_width
|
||||
proto.element_id = element_id
|
||||
302
myenv/lib/python3.11/site-packages/streamlit/elements/heading.py
Normal file
302
myenv/lib/python3.11/site-packages/streamlit/elements/heading.py
Normal file
@@ -0,0 +1,302 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Heading_pb2 import Heading as HeadingProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class HeadingProtoTag(Enum):
|
||||
TITLE_TAG = "h1"
|
||||
HEADER_TAG = "h2"
|
||||
SUBHEADER_TAG = "h3"
|
||||
|
||||
|
||||
Anchor: TypeAlias = Union[str, Literal[False], None]
|
||||
Divider: TypeAlias = Union[bool, str, None]
|
||||
|
||||
|
||||
class HeadingMixin:
|
||||
@gather_metrics("header")
|
||||
def header(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
divider: Divider = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in header formatting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
anchor : str or False
|
||||
The anchor name of the header that can be accessed with #anchor
|
||||
in the URL. If omitted, it generates an anchor using the body.
|
||||
If False, the anchor is not shown in the UI.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the header. If this is
|
||||
``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
divider : bool or “blue”, “green”, “orange”, “red”, “violet”, “gray”/"grey", or “rainbow”
|
||||
Shows a colored divider below the header. If True, successive
|
||||
headers will cycle through divider colors. That is, the first
|
||||
header will have a blue line, the second header will have a
|
||||
green line, and so on. If a string, the color can be set to one of
|
||||
the following: blue, green, orange, red, violet, gray/grey, or
|
||||
rainbow.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.header("_Streamlit_ is :blue[cool] :sunglasses:")
|
||||
>>> st.header("This is a header with a divider", divider="gray")
|
||||
>>> st.header("These headers have rotating dividers", divider=True)
|
||||
>>> st.header("One", divider=True)
|
||||
>>> st.header("Two", divider=True)
|
||||
>>> st.header("Three", divider=True)
|
||||
>>> st.header("Four", divider=True)
|
||||
|
||||
.. output::
|
||||
https://doc-header.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
return self.dg._enqueue(
|
||||
"heading",
|
||||
HeadingMixin._create_heading_proto(
|
||||
tag=HeadingProtoTag.HEADER_TAG,
|
||||
body=body,
|
||||
anchor=anchor,
|
||||
help=help,
|
||||
divider=divider,
|
||||
),
|
||||
)
|
||||
|
||||
@gather_metrics("subheader")
|
||||
def subheader(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
divider: Divider = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in subheader formatting.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
anchor : str or False
|
||||
The anchor name of the header that can be accessed with #anchor
|
||||
in the URL. If omitted, it generates an anchor using the body.
|
||||
If False, the anchor is not shown in the UI.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the subheader. If this is
|
||||
``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
divider : bool or “blue”, “green”, “orange”, “red”, “violet”, “gray”/"grey", or “rainbow”
|
||||
Shows a colored divider below the header. If True, successive
|
||||
headers will cycle through divider colors. That is, the first
|
||||
header will have a blue line, the second header will have a
|
||||
green line, and so on. If a string, the color can be set to one of
|
||||
the following: blue, green, orange, red, violet, gray/grey, or
|
||||
rainbow.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.subheader("_Streamlit_ is :blue[cool] :sunglasses:")
|
||||
>>> st.subheader("This is a subheader with a divider", divider="gray")
|
||||
>>> st.subheader("These subheaders have rotating dividers", divider=True)
|
||||
>>> st.subheader("One", divider=True)
|
||||
>>> st.subheader("Two", divider=True)
|
||||
>>> st.subheader("Three", divider=True)
|
||||
>>> st.subheader("Four", divider=True)
|
||||
|
||||
.. output::
|
||||
https://doc-subheader.streamlit.app/
|
||||
height: 500px
|
||||
|
||||
"""
|
||||
return self.dg._enqueue(
|
||||
"heading",
|
||||
HeadingMixin._create_heading_proto(
|
||||
tag=HeadingProtoTag.SUBHEADER_TAG,
|
||||
body=body,
|
||||
anchor=anchor,
|
||||
help=help,
|
||||
divider=divider,
|
||||
),
|
||||
)
|
||||
|
||||
@gather_metrics("title")
|
||||
def title(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in title formatting.
|
||||
|
||||
Each document should have a single `st.title()`, although this is not
|
||||
enforced.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
anchor : str or False
|
||||
The anchor name of the header that can be accessed with #anchor
|
||||
in the URL. If omitted, it generates an anchor using the body.
|
||||
If False, the anchor is not shown in the UI.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the title. If this is
|
||||
``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.title("This is a title")
|
||||
>>> st.title("_Streamlit_ is :blue[cool] :sunglasses:")
|
||||
|
||||
.. output::
|
||||
https://doc-title.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
return self.dg._enqueue(
|
||||
"heading",
|
||||
HeadingMixin._create_heading_proto(
|
||||
tag=HeadingProtoTag.TITLE_TAG, body=body, anchor=anchor, help=help
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
@staticmethod
|
||||
def _handle_divider_color(divider: Divider) -> str:
|
||||
if divider is True:
|
||||
return "auto"
|
||||
valid_colors = [
|
||||
"blue",
|
||||
"green",
|
||||
"orange",
|
||||
"red",
|
||||
"violet",
|
||||
"gray",
|
||||
"grey",
|
||||
"rainbow",
|
||||
]
|
||||
if divider in valid_colors:
|
||||
return cast("str", divider)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Divider parameter has invalid value: `{divider}`. Please choose from: {', '.join(valid_colors)}."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _create_heading_proto(
|
||||
tag: HeadingProtoTag,
|
||||
body: SupportsStr,
|
||||
anchor: Anchor = None,
|
||||
help: str | None = None,
|
||||
divider: Divider = False,
|
||||
) -> HeadingProto:
|
||||
proto = HeadingProto()
|
||||
proto.tag = tag.value
|
||||
proto.body = clean_text(body)
|
||||
if divider:
|
||||
proto.divider = HeadingMixin._handle_divider_color(divider)
|
||||
if anchor is not None:
|
||||
if anchor is False:
|
||||
proto.hide_anchor = True
|
||||
elif isinstance(anchor, str):
|
||||
proto.anchor = anchor
|
||||
elif anchor is True: # type: ignore
|
||||
raise StreamlitAPIException(
|
||||
"Anchor parameter has invalid value: %s. "
|
||||
"Supported values: None, any string or False" % anchor
|
||||
)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Anchor parameter has invalid type: %s. "
|
||||
"Supported values: None, any string or False"
|
||||
% type(anchor).__name__
|
||||
)
|
||||
|
||||
if help:
|
||||
proto.help = help
|
||||
return proto
|
||||
105
myenv/lib/python3.11/site-packages/streamlit/elements/html.py
Normal file
105
myenv/lib/python3.11/site-packages/streamlit/elements/html.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from streamlit.proto.Html_pb2 import Html as HtmlProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
from streamlit.type_util import SupportsReprHtml, SupportsStr, has_callable_attr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class HtmlMixin:
|
||||
@gather_metrics("html")
|
||||
def html(
|
||||
self,
|
||||
body: str | Path | SupportsStr | SupportsReprHtml,
|
||||
) -> DeltaGenerator:
|
||||
"""Insert HTML into your app.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability. We sanitize HTML with `DOMPurify
|
||||
<https://github.com/cure53/DOMPurify>`_, but inserting HTML remains a
|
||||
developer risk. Passing untrusted code to ``st.html`` or dynamically
|
||||
loading external code can increase the risk of vulnerabilities in your
|
||||
app.
|
||||
|
||||
``st.html`` content is **not** iframed. Executing JavaScript is not
|
||||
supported at this time.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : any
|
||||
The HTML code to insert. This can be one of the following:
|
||||
|
||||
- A string of HTML code.
|
||||
- A path to a local file with HTML code. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- Any object. If ``body`` is not a string or path, Streamlit will
|
||||
convert the object to a string. ``body._repr_html_()`` takes
|
||||
precedence over ``str(body)`` when available.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.html(
|
||||
... "<p><span style='text-decoration: line-through double red;'>Oops</span>!</p>"
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-html.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
"""
|
||||
html_proto = HtmlProto()
|
||||
|
||||
# If body supports _repr_html_, use that.
|
||||
if has_callable_attr(body, "_repr_html_"):
|
||||
html_proto.body = cast("SupportsReprHtml", body)._repr_html_()
|
||||
|
||||
# Check if the body is a file path. May include filesystem lookup.
|
||||
elif isinstance(body, Path) or _is_file(body):
|
||||
with open(cast("str", body), encoding="utf-8") as f:
|
||||
html_proto.body = f.read()
|
||||
|
||||
# OK, let's just try converting to string and hope for the best.
|
||||
else:
|
||||
html_proto.body = clean_text(cast("SupportsStr", body))
|
||||
|
||||
return self.dg._enqueue("html", html_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _is_file(obj: Any) -> bool:
|
||||
"""Checks if obj is a file, and doesn't throw if not.
|
||||
|
||||
The "not throwing" part is important!
|
||||
"""
|
||||
try:
|
||||
return os.path.isfile(obj)
|
||||
except TypeError:
|
||||
return False
|
||||
191
myenv/lib/python3.11/site-packages/streamlit/elements/iframe.py
Normal file
191
myenv/lib/python3.11/site-packages/streamlit/elements/iframe.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.IFrame_pb2 import IFrame as IFrameProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class IframeMixin:
|
||||
@gather_metrics("_iframe")
|
||||
def _iframe(
|
||||
self,
|
||||
src: str,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
scrolling: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Load a remote URL in an iframe.
|
||||
|
||||
To use this function, import it from the ``streamlit.components.v1``
|
||||
module.
|
||||
|
||||
.. warning::
|
||||
Using ``st.components.v1.iframe`` directly (instead of importing
|
||||
its module) is deprecated and will be disallowed in a later version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : str
|
||||
The URL of the page to embed.
|
||||
|
||||
width : int
|
||||
The width of the iframe in CSS pixels. By default, this is the
|
||||
app's default element width.
|
||||
|
||||
height : int
|
||||
The height of the frame in CSS pixels. By default, this is ``150``.
|
||||
|
||||
scrolling : bool
|
||||
Whether to allow scrolling in the iframe. If this ``False``
|
||||
(default), Streamlit crops any content larger than the iframe and
|
||||
does not show a scrollbar. If this is ``True``, Streamlit shows a
|
||||
scrollbar when the content is larger than the iframe.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
>>> import streamlit.components.v1 as components
|
||||
>>>
|
||||
>>> components.iframe("https://example.com", height=500)
|
||||
|
||||
"""
|
||||
iframe_proto = IFrameProto()
|
||||
marshall(
|
||||
iframe_proto,
|
||||
src=src,
|
||||
width=width,
|
||||
height=height,
|
||||
scrolling=scrolling,
|
||||
)
|
||||
return self.dg._enqueue("iframe", iframe_proto)
|
||||
|
||||
@gather_metrics("_html")
|
||||
def _html(
|
||||
self,
|
||||
html: str,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
scrolling: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an HTML string in an iframe.
|
||||
|
||||
To use this function, import it from the ``streamlit.components.v1``
|
||||
module.
|
||||
|
||||
If you want to insert HTML text into your app without an iframe, try
|
||||
``st.html`` instead.
|
||||
|
||||
.. warning::
|
||||
Using ``st.components.v1.html`` directly (instead of importing
|
||||
its module) is deprecated and will be disallowed in a later version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
html : str
|
||||
The HTML string to embed in the iframe.
|
||||
|
||||
width : int
|
||||
The width of the iframe in CSS pixels. By default, this is the
|
||||
app's default element width.
|
||||
|
||||
height : int
|
||||
The height of the frame in CSS pixels. By default, this is ``150``.
|
||||
|
||||
scrolling : bool
|
||||
Whether to allow scrolling in the iframe. If this ``False``
|
||||
(default), Streamlit crops any content larger than the iframe and
|
||||
does not show a scrollbar. If this is ``True``, Streamlit shows a
|
||||
scrollbar when the content is larger than the iframe.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
>>> import streamlit.components.v1 as components
|
||||
>>>
|
||||
>>> components.html(
|
||||
>>> "<p><span style='text-decoration: line-through double red;'>Oops</span>!</p>"
|
||||
>>> )
|
||||
|
||||
"""
|
||||
iframe_proto = IFrameProto()
|
||||
marshall(
|
||||
iframe_proto,
|
||||
srcdoc=html,
|
||||
width=width,
|
||||
height=height,
|
||||
scrolling=scrolling,
|
||||
)
|
||||
return self.dg._enqueue("iframe", iframe_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
proto: IFrameProto,
|
||||
src: str | None = None,
|
||||
srcdoc: str | None = None,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
scrolling: bool = False,
|
||||
) -> None:
|
||||
"""Marshalls data into an IFrame proto.
|
||||
|
||||
These parameters correspond directly to <iframe> attributes, which are
|
||||
described in more detail at
|
||||
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : IFrame protobuf
|
||||
The protobuf object to marshall data into.
|
||||
src : str
|
||||
The URL of the page to embed.
|
||||
srcdoc : str
|
||||
Inline HTML to embed. Overrides src.
|
||||
width : int
|
||||
The width of the frame in CSS pixels. Defaults to the app's
|
||||
default element width.
|
||||
height : int
|
||||
The height of the frame in CSS pixels. Defaults to 150.
|
||||
scrolling : bool
|
||||
If true, show a scrollbar when the content is larger than the iframe.
|
||||
Otherwise, never show a scrollbar.
|
||||
|
||||
"""
|
||||
if src is not None:
|
||||
proto.src = src
|
||||
|
||||
if srcdoc is not None:
|
||||
proto.srcdoc = srcdoc
|
||||
|
||||
if width is not None:
|
||||
proto.width = width
|
||||
proto.has_width = True
|
||||
|
||||
if height is not None:
|
||||
proto.height = height
|
||||
else:
|
||||
proto.height = 150
|
||||
|
||||
proto.scrolling = scrolling
|
||||
196
myenv/lib/python3.11/site-packages/streamlit/elements/image.py
Normal file
196
myenv/lib/python3.11/site-packages/streamlit/elements/image.py
Normal file
@@ -0,0 +1,196 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Some casts in this file are only occasionally necessary depending on the
|
||||
# user's Python version, and mypy doesn't have a good way of toggling this
|
||||
# specific config option at a per-line level.
|
||||
# mypy: no-warn-unused-ignores
|
||||
|
||||
"""Image marshalling."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.deprecation_util import show_deprecation_warning
|
||||
from streamlit.elements.lib.image_utils import (
|
||||
Channels,
|
||||
ImageFormatOrAuto,
|
||||
ImageOrImageList,
|
||||
WidthBehavior,
|
||||
marshall_images,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
UseColumnWith: TypeAlias = Union[Literal["auto", "always", "never"], bool, None]
|
||||
|
||||
|
||||
class ImageMixin:
|
||||
@gather_metrics("image")
|
||||
def image(
|
||||
self,
|
||||
image: ImageOrImageList,
|
||||
# TODO: Narrow type of caption, dependent on type of image,
|
||||
# by way of overload
|
||||
caption: str | list[str] | None = None,
|
||||
width: int | None = None,
|
||||
use_column_width: UseColumnWith = None,
|
||||
clamp: bool = False,
|
||||
channels: Channels = "RGB",
|
||||
output_format: ImageFormatOrAuto = "auto",
|
||||
*,
|
||||
use_container_width: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an image or list of images.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
image : numpy.ndarray, BytesIO, str, Path, or list of these
|
||||
The image to display. This can be one of the following:
|
||||
|
||||
- A URL (string) for a hosted image.
|
||||
- A path to a local image file. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- An SVG string like ``<svg xmlns=...</svg>``.
|
||||
- A byte array defining an image. This includes monochrome images of
|
||||
shape (w,h) or (w,h,1), color images of shape (w,h,3), or RGBA
|
||||
images of shape (w,h,4), where w and h are the image width and
|
||||
height, respectively.
|
||||
- A list of any of the above. Streamlit displays the list as a
|
||||
row of images that overflow to additional rows as needed.
|
||||
caption : str or list of str
|
||||
Image caption(s). If this is ``None`` (default), no caption is
|
||||
displayed. If ``image`` is a list of multiple images, ``caption``
|
||||
must be a list of captions (one caption for each image) or
|
||||
``None``.
|
||||
|
||||
Captions can optionally contain GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
width : int or None
|
||||
Image width. If this is ``None`` (default), Streamlit will use the
|
||||
image's native width, up to the width of the parent container.
|
||||
When using an SVG image without a default width, you should declare
|
||||
``width`` or use ``use_container_width=True``.
|
||||
use_column_width : "auto", "always", "never", or bool
|
||||
If "auto", set the image's width to its natural size,
|
||||
but do not exceed the width of the column.
|
||||
If "always" or True, set the image's width to the column width.
|
||||
If "never" or False, set the image's width to its natural size.
|
||||
Note: if set, `use_column_width` takes precedence over the `width` parameter.
|
||||
clamp : bool
|
||||
Whether to clamp image pixel values to a valid range (0-255 per
|
||||
channel). This is only used for byte array images; the parameter is
|
||||
ignored for image URLs and files. If this is ``False`` (default)
|
||||
and an image has an out-of-range value, a ``RuntimeError`` will be
|
||||
raised.
|
||||
channels : "RGB" or "BGR"
|
||||
The color format when ``image`` is an ``nd.array``. This is ignored
|
||||
for other image types. If this is ``"RGB"`` (default),
|
||||
``image[:, :, 0]`` is the red channel, ``image[:, :, 1]`` is the
|
||||
green channel, and ``image[:, :, 2]`` is the blue channel. For
|
||||
images coming from libraries like OpenCV, you should set this to
|
||||
``"BGR"`` instead.
|
||||
output_format : "JPEG", "PNG", or "auto"
|
||||
The output format to use when transferring the image data. If this
|
||||
is ``"auto"`` (default), Streamlit identifies the compression type
|
||||
based on the type and format of the image. Photos should use the
|
||||
``"JPEG"`` format for lossy compression while diagrams should use
|
||||
the ``"PNG"`` format for lossless compression.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override ``width`` with the width of the parent
|
||||
container. If ``use_container_width`` is ``False`` (default),
|
||||
Streamlit sets the image's width according to ``width``. If
|
||||
``use_container_width`` is ``True``, Streamlit sets the width of
|
||||
the image to match the width of the parent container.
|
||||
|
||||
.. deprecated::
|
||||
``use_column_width`` is deprecated and will be removed in a future
|
||||
release. Please use the ``use_container_width`` parameter instead.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> st.image("sunrise.jpg", caption="Sunrise by the mountains")
|
||||
|
||||
.. output::
|
||||
https://doc-image.streamlit.app/
|
||||
height: 710px
|
||||
|
||||
"""
|
||||
|
||||
if use_container_width is True and use_column_width is not None:
|
||||
raise StreamlitAPIException(
|
||||
"`use_container_width` and `use_column_width` cannot be set at the same time.",
|
||||
"Please utilize `use_container_width` since `use_column_width` is deprecated.",
|
||||
)
|
||||
|
||||
image_width: int = (
|
||||
WidthBehavior.ORIGINAL if (width is None or width <= 0) else width
|
||||
)
|
||||
|
||||
if use_column_width is not None:
|
||||
show_deprecation_warning(
|
||||
"The `use_column_width` parameter has been deprecated and will be removed "
|
||||
"in a future release. Please utilize the `use_container_width` parameter instead."
|
||||
)
|
||||
|
||||
if use_column_width == "auto":
|
||||
image_width = WidthBehavior.AUTO
|
||||
elif use_column_width == "always" or use_column_width is True:
|
||||
image_width = WidthBehavior.COLUMN
|
||||
elif use_column_width == "never" or use_column_width is False:
|
||||
image_width = WidthBehavior.ORIGINAL
|
||||
|
||||
else:
|
||||
if use_container_width is True:
|
||||
image_width = WidthBehavior.MAX_IMAGE_OR_CONTAINER
|
||||
elif image_width is not None and image_width > 0:
|
||||
# Use the given width. It will be capped on the frontend if it
|
||||
# exceeds the container width.
|
||||
pass
|
||||
elif use_container_width is False:
|
||||
image_width = WidthBehavior.MIN_IMAGE_OR_CONTAINER
|
||||
|
||||
image_list_proto = ImageListProto()
|
||||
marshall_images(
|
||||
self.dg._get_delta_path_str(),
|
||||
image,
|
||||
caption,
|
||||
image_width,
|
||||
image_list_proto,
|
||||
clamp,
|
||||
channels,
|
||||
output_format,
|
||||
)
|
||||
return self.dg._enqueue("imgs", image_list_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
139
myenv/lib/python3.11/site-packages/streamlit/elements/json.py
Normal file
139
myenv/lib/python3.11/site-packages/streamlit/elements/json.py
Normal file
@@ -0,0 +1,139 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import types
|
||||
from collections import ChainMap, UserDict
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from streamlit.proto.Json_pb2 import Json as JsonProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.type_util import (
|
||||
is_custom_dict,
|
||||
is_list_like,
|
||||
is_namedtuple,
|
||||
is_pydantic_model,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
def _ensure_serialization(o: object) -> str | list[Any]:
|
||||
"""A repr function for json.dumps default arg, which tries to serialize sets
|
||||
as lists.
|
||||
"""
|
||||
return list(o) if isinstance(o, set) else repr(o)
|
||||
|
||||
|
||||
class JsonMixin:
|
||||
@gather_metrics("json")
|
||||
def json(
|
||||
self,
|
||||
body: object,
|
||||
*, # keyword-only arguments:
|
||||
expanded: bool | int = True,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an object or string as a pretty-printed, interactive JSON string.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : object or str
|
||||
The object to print as JSON. All referenced objects should be
|
||||
serializable to JSON as well. If object is a string, we assume it
|
||||
contains serialized JSON.
|
||||
|
||||
expanded : bool or int
|
||||
The initial expansion state of the JSON element. This can be one
|
||||
of the following:
|
||||
|
||||
- ``True`` (default): The element is fully expanded.
|
||||
- ``False``: The element is fully collapsed.
|
||||
- An integer: The element is expanded to the depth specified. The
|
||||
integer must be non-negative. ``expanded=0`` is equivalent to
|
||||
``expanded=False``.
|
||||
|
||||
Regardless of the initial expansion state, users can collapse or
|
||||
expand any key-value pair to show or hide any part of the object.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.json(
|
||||
... {
|
||||
... "foo": "bar",
|
||||
... "stuff": [
|
||||
... "stuff 1",
|
||||
... "stuff 2",
|
||||
... "stuff 3",
|
||||
... ],
|
||||
... "level1": {"level2": {"level3": {"a": "b"}}},
|
||||
... },
|
||||
... expanded=2,
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-json.streamlit.app/
|
||||
height: 385px
|
||||
|
||||
"""
|
||||
|
||||
if is_custom_dict(body):
|
||||
body = body.to_dict()
|
||||
|
||||
if is_namedtuple(body):
|
||||
body = body._asdict()
|
||||
|
||||
if isinstance(
|
||||
body, (ChainMap, types.MappingProxyType, UserDict)
|
||||
) or is_pydantic_model(body):
|
||||
body = dict(body) # type: ignore
|
||||
|
||||
if is_list_like(body):
|
||||
body = list(body)
|
||||
|
||||
if not isinstance(body, str):
|
||||
try:
|
||||
# Serialize body to string and try to interpret sets as lists
|
||||
body = json.dumps(body, default=_ensure_serialization)
|
||||
except TypeError as err:
|
||||
self.dg.warning(
|
||||
"Warning: this data structure was not fully serializable as "
|
||||
f"JSON due to one or more unexpected keys. (Error was: {err})"
|
||||
)
|
||||
body = json.dumps(body, skipkeys=True, default=_ensure_serialization)
|
||||
|
||||
json_proto = JsonProto()
|
||||
json_proto.body = body
|
||||
|
||||
if isinstance(expanded, bool):
|
||||
json_proto.expanded = expanded
|
||||
elif isinstance(expanded, int):
|
||||
json_proto.expanded = True
|
||||
json_proto.max_expand_depth = expanded
|
||||
else:
|
||||
raise TypeError(
|
||||
f"The type {str(type(expanded))} of `expanded` is not supported"
|
||||
", must be bool or int."
|
||||
)
|
||||
|
||||
return self.dg._enqueue("json", json_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
874
myenv/lib/python3.11/site-packages/streamlit/elements/layouts.py
Normal file
874
myenv/lib/python3.11/site-packages/streamlit/elements/layouts.py
Normal file
@@ -0,0 +1,874 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.delta_generator_singletons import get_dg_singleton_instance
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import (
|
||||
StreamlitAPIException,
|
||||
StreamlitInvalidColumnGapError,
|
||||
StreamlitInvalidColumnSpecError,
|
||||
StreamlitInvalidVerticalAlignmentError,
|
||||
)
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import validate_icon_or_emoji
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.elements.lib.dialog import Dialog
|
||||
from streamlit.elements.lib.mutable_status_container import StatusContainer
|
||||
|
||||
SpecType: TypeAlias = Union[int, Sequence[Union[int, float]]]
|
||||
|
||||
|
||||
class LayoutsMixin:
|
||||
@gather_metrics("container")
|
||||
def container(
|
||||
self,
|
||||
*,
|
||||
height: int | None = None,
|
||||
border: bool | None = None,
|
||||
key: Key | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Insert a multi-element container.
|
||||
|
||||
Inserts an invisible container into your app that can be used to hold
|
||||
multiple elements. This allows you to, for example, insert multiple
|
||||
elements into your app out of order.
|
||||
|
||||
To add elements to the returned container, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
height : int or None
|
||||
Desired height of the container expressed in pixels. If ``None`` (default)
|
||||
the container grows to fit its content. If a fixed height, scrolling is
|
||||
enabled for large content and a grey border is shown around the container
|
||||
to visually separate its scroll surface from the rest of the app.
|
||||
|
||||
.. note::
|
||||
Use containers with scroll sparingly. If you do, try to keep
|
||||
the height small (below 500 pixels). Otherwise, the scroll
|
||||
surface of the container might cover the majority of the screen
|
||||
on mobile devices, which makes it hard to scroll the rest of the app.
|
||||
|
||||
border : bool or None
|
||||
Whether to show a border around the container. If ``None`` (default), a
|
||||
border is shown if the container is set to a fixed height and not
|
||||
shown otherwise.
|
||||
|
||||
key : str or None
|
||||
An optional string to give this container a stable identity.
|
||||
|
||||
Additionally, if ``key`` is provided, it will be used as CSS
|
||||
class name prefixed with ``st-key-``.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
Inserting elements using ``with`` notation:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.container():
|
||||
... st.write("This is inside the container")
|
||||
...
|
||||
... # You can call any Streamlit command, including custom components:
|
||||
... st.bar_chart(np.random.randn(50, 3))
|
||||
>>>
|
||||
>>> st.write("This is outside the container")
|
||||
|
||||
.. output ::
|
||||
https://doc-container1.streamlit.app/
|
||||
height: 520px
|
||||
|
||||
Inserting elements out of order:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> container = st.container(border=True)
|
||||
>>> container.write("This is inside the container")
|
||||
>>> st.write("This is outside the container")
|
||||
>>>
|
||||
>>> # Now insert some more in the container
|
||||
>>> container.write("This is inside too")
|
||||
|
||||
.. output ::
|
||||
https://doc-container2.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
Using ``height`` to make a grid:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> row1 = st.columns(3)
|
||||
>>> row2 = st.columns(3)
|
||||
>>>
|
||||
>>> for col in row1 + row2:
|
||||
>>> tile = col.container(height=120)
|
||||
>>> tile.title(":balloon:")
|
||||
|
||||
.. output ::
|
||||
https://doc-container3.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
Using ``height`` to create a scrolling container for long content:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> long_text = "Lorem ipsum. " * 1000
|
||||
>>>
|
||||
>>> with st.container(height=300):
|
||||
>>> st.markdown(long_text)
|
||||
|
||||
.. output ::
|
||||
https://doc-container4.streamlit.app/
|
||||
height: 400px
|
||||
|
||||
"""
|
||||
key = to_key(key)
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = False
|
||||
block_proto.vertical.border = border or False
|
||||
|
||||
if height:
|
||||
# Activate scrolling container behavior:
|
||||
block_proto.allow_empty = True
|
||||
block_proto.vertical.height = height
|
||||
if border is None:
|
||||
# If border is None, we activated the
|
||||
# border as default setting for scrolling
|
||||
# containers.
|
||||
block_proto.vertical.border = True
|
||||
|
||||
if key:
|
||||
# At the moment, the ID is only used for extracting the
|
||||
# key on the frontend and setting it as CSS class.
|
||||
# There are plans to use the ID for other container features
|
||||
# in the future. This might require including more container
|
||||
# parameters in the ID calculation.
|
||||
block_proto.id = compute_and_register_element_id(
|
||||
"container", user_key=key, form_id=None
|
||||
)
|
||||
|
||||
return self.dg._block(block_proto)
|
||||
|
||||
@gather_metrics("columns")
|
||||
def columns(
|
||||
self,
|
||||
spec: SpecType,
|
||||
*,
|
||||
gap: Literal["small", "medium", "large"] = "small",
|
||||
vertical_alignment: Literal["top", "center", "bottom"] = "top",
|
||||
border: bool = False,
|
||||
) -> list[DeltaGenerator]:
|
||||
"""Insert containers laid out as side-by-side columns.
|
||||
|
||||
Inserts a number of multi-element containers laid out side-by-side and
|
||||
returns a list of container objects.
|
||||
|
||||
To add elements to the returned containers, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
Columns can only be placed inside other columns up to one level of nesting.
|
||||
|
||||
.. warning::
|
||||
Columns cannot be placed inside other columns in the sidebar. This
|
||||
is only possible in the main area of the app.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
spec : int or Iterable of numbers
|
||||
Controls the number and width of columns to insert. Can be one of:
|
||||
|
||||
- An integer that specifies the number of columns. All columns have equal
|
||||
width in this case.
|
||||
- An Iterable of numbers (int or float) that specify the relative width of
|
||||
each column. E.g. ``[0.7, 0.3]`` creates two columns where the first
|
||||
one takes up 70% of the available with and the second one takes up 30%.
|
||||
Or ``[1, 2, 3]`` creates three columns where the second one is two times
|
||||
the width of the first one, and the third one is three times that width.
|
||||
|
||||
gap : "small", "medium", or "large"
|
||||
The size of the gap between the columns. The default is ``"small"``.
|
||||
|
||||
vertical_alignment : "top", "center", or "bottom"
|
||||
The vertical alignment of the content inside the columns. The
|
||||
default is ``"top"``.
|
||||
|
||||
border : bool
|
||||
Whether to show a border around the column containers. If this is
|
||||
``False`` (default), no border is shown. If this is ``True``, a
|
||||
border is shown around each column.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of containers
|
||||
A list of container objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Use context management**
|
||||
|
||||
You can use the ``with`` statement to insert any element into a column:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> col1, col2, col3 = st.columns(3)
|
||||
>>>
|
||||
>>> with col1:
|
||||
... st.header("A cat")
|
||||
... st.image("https://static.streamlit.io/examples/cat.jpg")
|
||||
>>>
|
||||
>>> with col2:
|
||||
... st.header("A dog")
|
||||
... st.image("https://static.streamlit.io/examples/dog.jpg")
|
||||
>>>
|
||||
>>> with col3:
|
||||
... st.header("An owl")
|
||||
... st.image("https://static.streamlit.io/examples/owl.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns1.streamlit.app/
|
||||
height: 620px
|
||||
|
||||
|
||||
**Example 2: Use commands as container methods**
|
||||
|
||||
You can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> col1, col2 = st.columns([3, 1])
|
||||
>>> data = np.random.randn(10, 1)
|
||||
>>>
|
||||
>>> col1.subheader("A wide column with a chart")
|
||||
>>> col1.line_chart(data)
|
||||
>>>
|
||||
>>> col2.subheader("A narrow column with the data")
|
||||
>>> col2.write(data)
|
||||
|
||||
.. output ::
|
||||
https://doc-columns2.streamlit.app/
|
||||
height: 550px
|
||||
|
||||
**Example 3: Align widgets**
|
||||
|
||||
Use ``vertical_alignment="bottom"`` to align widgets.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> left, middle, right = st.columns(3, vertical_alignment="bottom")
|
||||
>>>
|
||||
>>> left.text_input("Write something")
|
||||
>>> middle.button("Click me", use_container_width=True)
|
||||
>>> right.checkbox("Check me")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns-bottom-widgets.streamlit.app/
|
||||
height: 200px
|
||||
|
||||
**Example 4: Use vertical alignment to create grids**
|
||||
|
||||
Adjust vertical alignment to customize your grid layouts.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> vertical_alignment = st.selectbox(
|
||||
>>> "Vertical alignment", ["top", "center", "bottom"], index=2
|
||||
>>> )
|
||||
>>>
|
||||
>>> left, middle, right = st.columns(3, vertical_alignment=vertical_alignment)
|
||||
>>> left.image("https://static.streamlit.io/examples/cat.jpg")
|
||||
>>> middle.image("https://static.streamlit.io/examples/dog.jpg")
|
||||
>>> right.image("https://static.streamlit.io/examples/owl.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns-vertical-alignment.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
**Example 5: Add borders**
|
||||
|
||||
Add borders to your columns instead of nested containers for consistent
|
||||
heights.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> left, middle, right = st.columns(3, border=True)
|
||||
>>>
|
||||
>>> left.markdown("Lorem ipsum " * 10)
|
||||
>>> middle.markdown("Lorem ipsum " * 5)
|
||||
>>> right.markdown("Lorem ipsum ")
|
||||
|
||||
.. output ::
|
||||
https://doc-columns-borders.streamlit.app/
|
||||
height: 250px
|
||||
|
||||
"""
|
||||
weights = spec
|
||||
if isinstance(weights, int):
|
||||
# If the user provided a single number, expand into equal weights.
|
||||
# E.g. (1,) * 3 => (1, 1, 1)
|
||||
# NOTE: A negative/zero spec will expand into an empty tuple.
|
||||
weights = (1,) * weights
|
||||
|
||||
if len(weights) == 0 or any(weight <= 0 for weight in weights):
|
||||
raise StreamlitInvalidColumnSpecError()
|
||||
|
||||
vertical_alignment_mapping: dict[
|
||||
str, BlockProto.Column.VerticalAlignment.ValueType
|
||||
] = {
|
||||
"top": BlockProto.Column.VerticalAlignment.TOP,
|
||||
"center": BlockProto.Column.VerticalAlignment.CENTER,
|
||||
"bottom": BlockProto.Column.VerticalAlignment.BOTTOM,
|
||||
}
|
||||
|
||||
if vertical_alignment not in vertical_alignment_mapping:
|
||||
raise StreamlitInvalidVerticalAlignmentError(
|
||||
vertical_alignment=vertical_alignment
|
||||
)
|
||||
|
||||
def column_gap(gap):
|
||||
if isinstance(gap, str):
|
||||
gap_size = gap.lower()
|
||||
valid_sizes = ["small", "medium", "large"]
|
||||
|
||||
if gap_size in valid_sizes:
|
||||
return gap_size
|
||||
|
||||
raise StreamlitInvalidColumnGapError(gap=gap)
|
||||
|
||||
gap_size = column_gap(gap)
|
||||
|
||||
def column_proto(normalized_weight: float) -> BlockProto:
|
||||
col_proto = BlockProto()
|
||||
col_proto.column.weight = normalized_weight
|
||||
col_proto.column.gap = gap_size
|
||||
col_proto.column.vertical_alignment = vertical_alignment_mapping[
|
||||
vertical_alignment
|
||||
]
|
||||
col_proto.column.show_border = border
|
||||
col_proto.allow_empty = True
|
||||
return col_proto
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.horizontal.gap = gap_size
|
||||
row = self.dg._block(block_proto)
|
||||
total_weight = sum(weights)
|
||||
return [row._block(column_proto(w / total_weight)) for w in weights]
|
||||
|
||||
@gather_metrics("tabs")
|
||||
def tabs(self, tabs: Sequence[str]) -> Sequence[DeltaGenerator]:
|
||||
r"""Insert containers separated into tabs.
|
||||
|
||||
Inserts a number of multi-element containers as tabs.
|
||||
Tabs are a navigational element that allows users to easily
|
||||
move between groups of related content.
|
||||
|
||||
To add elements to the returned containers, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
.. warning::
|
||||
All the content of every tab is always sent to and rendered on the frontend.
|
||||
Conditional rendering is currently not supported.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tabs : list of str
|
||||
Creates a tab for each string in the list. The first tab is selected
|
||||
by default. The string is used as the name of the tab and can
|
||||
optionally contain GitHub-flavored Markdown of the following types:
|
||||
Bold, Italics, Strikethroughs, Inline Code, Links, and Images.
|
||||
Images display like icons, with a max height equal to the font
|
||||
height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of containers
|
||||
A list of container objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into a tab:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> tab1, tab2, tab3 = st.tabs(["Cat", "Dog", "Owl"])
|
||||
>>>
|
||||
>>> with tab1:
|
||||
... st.header("A cat")
|
||||
... st.image("https://static.streamlit.io/examples/cat.jpg", width=200)
|
||||
>>> with tab2:
|
||||
... st.header("A dog")
|
||||
... st.image("https://static.streamlit.io/examples/dog.jpg", width=200)
|
||||
>>> with tab3:
|
||||
... st.header("An owl")
|
||||
... st.image("https://static.streamlit.io/examples/owl.jpg", width=200)
|
||||
|
||||
.. output ::
|
||||
https://doc-tabs1.streamlit.app/
|
||||
height: 620px
|
||||
|
||||
Or you can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> tab1, tab2 = st.tabs(["📈 Chart", "🗃 Data"])
|
||||
>>> data = np.random.randn(10, 1)
|
||||
>>>
|
||||
>>> tab1.subheader("A tab with a chart")
|
||||
>>> tab1.line_chart(data)
|
||||
>>>
|
||||
>>> tab2.subheader("A tab with the data")
|
||||
>>> tab2.write(data)
|
||||
|
||||
|
||||
.. output ::
|
||||
https://doc-tabs2.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
"""
|
||||
if not tabs:
|
||||
raise StreamlitAPIException(
|
||||
"The input argument to st.tabs must contain at least one tab label."
|
||||
)
|
||||
|
||||
if any(not isinstance(tab, str) for tab in tabs):
|
||||
raise StreamlitAPIException(
|
||||
"The tabs input list to st.tabs is only allowed to contain strings."
|
||||
)
|
||||
|
||||
def tab_proto(label: str) -> BlockProto:
|
||||
tab_proto = BlockProto()
|
||||
tab_proto.tab.label = label
|
||||
tab_proto.allow_empty = True
|
||||
return tab_proto
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.tab_container.SetInParent()
|
||||
tab_container = self.dg._block(block_proto)
|
||||
return tuple(tab_container._block(tab_proto(tab_label)) for tab_label in tabs)
|
||||
|
||||
@gather_metrics("expander")
|
||||
def expander(
|
||||
self,
|
||||
label: str,
|
||||
expanded: bool = False,
|
||||
*,
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
r"""Insert a multi-element container that can be expanded/collapsed.
|
||||
|
||||
Inserts a container into your app that can be used to hold multiple elements
|
||||
and can be expanded or collapsed by the user. When collapsed, all that is
|
||||
visible is the provided label.
|
||||
|
||||
To add elements to the returned container, you can use the ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See
|
||||
examples below.
|
||||
|
||||
.. warning::
|
||||
Currently, you may not put expanders inside another expander.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A string to use as the header for the expander. The label can optionally
|
||||
contain GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code, Links, and Images. Images display like
|
||||
icons, with a max height equal to the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
expanded : bool
|
||||
If True, initializes the expander in "expanded" state. Defaults to
|
||||
False (collapsed).
|
||||
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the expander label. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into an expander
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})
|
||||
>>>
|
||||
>>> with st.expander("See explanation"):
|
||||
... st.write('''
|
||||
... The chart above shows some numbers I picked for you.
|
||||
... I rolled actual dice for these, so they're *guaranteed* to
|
||||
... be random.
|
||||
... ''')
|
||||
... st.image("https://static.streamlit.io/examples/dice.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-expander.streamlit.app/
|
||||
height: 750px
|
||||
|
||||
Or you can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.bar_chart({"data": [1, 5, 2, 6, 2, 1]})
|
||||
>>>
|
||||
>>> expander = st.expander("See explanation")
|
||||
>>> expander.write('''
|
||||
... The chart above shows some numbers I picked for you.
|
||||
... I rolled actual dice for these, so they're *guaranteed* to
|
||||
... be random.
|
||||
... ''')
|
||||
>>> expander.image("https://static.streamlit.io/examples/dice.jpg")
|
||||
|
||||
.. output ::
|
||||
https://doc-expander.streamlit.app/
|
||||
height: 750px
|
||||
|
||||
"""
|
||||
if label is None:
|
||||
raise StreamlitAPIException("A label is required for an expander")
|
||||
|
||||
expandable_proto = BlockProto.Expandable()
|
||||
expandable_proto.expanded = expanded
|
||||
expandable_proto.label = label
|
||||
if icon is not None:
|
||||
expandable_proto.icon = validate_icon_or_emoji(icon)
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = False
|
||||
block_proto.expandable.CopyFrom(expandable_proto)
|
||||
|
||||
return self.dg._block(block_proto=block_proto)
|
||||
|
||||
@gather_metrics("popover")
|
||||
def popover(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
help: str | None = None,
|
||||
icon: str | None = None,
|
||||
disabled: bool = False,
|
||||
use_container_width: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
r"""Insert a popover container.
|
||||
|
||||
Inserts a multi-element container as a popover. It consists of a button-like
|
||||
element and a container that opens when the button is clicked.
|
||||
|
||||
Opening and closing the popover will not trigger a rerun. Interacting
|
||||
with widgets inside of an open popover will rerun the app while keeping
|
||||
the popover open. Clicking outside of the popover will close it.
|
||||
|
||||
To add elements to the returned container, you can use the "with"
|
||||
notation (preferred) or just call methods directly on the returned object.
|
||||
See examples below.
|
||||
|
||||
.. warning::
|
||||
You may not put a popover inside another popover.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The label of the button that opens the popover container.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed when the popover button is hovered
|
||||
over. If this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
icon : str
|
||||
An optional emoji or icon to display next to the button label. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the popover button if set to
|
||||
``True``. The default is ``False``.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to expand the button's width to fill its parent container.
|
||||
If ``use_container_width`` is ``False`` (default), Streamlit sizes
|
||||
the button to fit its contents. If ``use_container_width`` is
|
||||
``True``, the width of the button matches its parent container.
|
||||
|
||||
In both cases, if the contents of the button are wider than the
|
||||
parent container, the contents will line wrap.
|
||||
|
||||
The popover containter's minimimun width matches the width of its
|
||||
button. The popover container may be wider than its button to fit
|
||||
the container's contents.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into a popover:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.popover("Open popover"):
|
||||
>>> st.markdown("Hello World 👋")
|
||||
>>> name = st.text_input("What's your name?")
|
||||
>>>
|
||||
>>> st.write("Your name:", name)
|
||||
|
||||
.. output ::
|
||||
https://doc-popover.streamlit.app/
|
||||
height: 400px
|
||||
|
||||
Or you can just call methods directly on the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> popover = st.popover("Filter items")
|
||||
>>> red = popover.checkbox("Show red items.", True)
|
||||
>>> blue = popover.checkbox("Show blue items.", True)
|
||||
>>>
|
||||
>>> if red:
|
||||
... st.write(":red[This is a red item.]")
|
||||
>>> if blue:
|
||||
... st.write(":blue[This is a blue item.]")
|
||||
|
||||
.. output ::
|
||||
https://doc-popover2.streamlit.app/
|
||||
height: 400px
|
||||
|
||||
"""
|
||||
if label is None:
|
||||
raise StreamlitAPIException("A label is required for a popover")
|
||||
|
||||
popover_proto = BlockProto.Popover()
|
||||
popover_proto.label = label
|
||||
popover_proto.use_container_width = use_container_width
|
||||
popover_proto.disabled = disabled
|
||||
if help:
|
||||
popover_proto.help = str(help)
|
||||
if icon is not None:
|
||||
popover_proto.icon = validate_icon_or_emoji(icon)
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = True
|
||||
block_proto.popover.CopyFrom(popover_proto)
|
||||
|
||||
return self.dg._block(block_proto=block_proto)
|
||||
|
||||
@gather_metrics("status")
|
||||
def status(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
expanded: bool = False,
|
||||
state: Literal["running", "complete", "error"] = "running",
|
||||
) -> StatusContainer:
|
||||
r"""Insert a status container to display output from long-running tasks.
|
||||
|
||||
Inserts a container into your app that is typically used to show the status and
|
||||
details of a process or task. The container can hold multiple elements and can
|
||||
be expanded or collapsed by the user similar to ``st.expander``.
|
||||
When collapsed, all that is visible is the status icon and label.
|
||||
|
||||
The label, state, and expanded state can all be updated by calling ``.update()``
|
||||
on the returned object. To add elements to the returned container, you can
|
||||
use ``with`` notation (preferred) or just call methods directly on the returned
|
||||
object.
|
||||
|
||||
By default, ``st.status()`` initializes in the "running" state. When called using
|
||||
``with`` notation, it automatically updates to the "complete" state at the end
|
||||
of the "with" block. See examples below for more details.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The initial label of the status container. The label can optionally
|
||||
contain GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code, Links, and Images. Images display like
|
||||
icons, with a max height equal to the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
expanded : bool
|
||||
If True, initializes the status container in "expanded" state. Defaults to
|
||||
False (collapsed).
|
||||
|
||||
state : "running", "complete", or "error"
|
||||
The initial state of the status container which determines which icon is
|
||||
shown:
|
||||
|
||||
- ``running`` (default): A spinner icon is shown.
|
||||
|
||||
- ``complete``: A checkmark icon is shown.
|
||||
|
||||
- ``error``: An error icon is shown.
|
||||
|
||||
Returns
|
||||
-------
|
||||
StatusContainer
|
||||
A mutable status container that can hold multiple elements. The label, state,
|
||||
and expanded state can be updated after creation via ``.update()``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use the ``with`` notation to insert any element into an status container:
|
||||
|
||||
>>> import time
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.status("Downloading data..."):
|
||||
... st.write("Searching for data...")
|
||||
... time.sleep(2)
|
||||
... st.write("Found URL.")
|
||||
... time.sleep(1)
|
||||
... st.write("Downloading data...")
|
||||
... time.sleep(1)
|
||||
>>>
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output ::
|
||||
https://doc-status.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
You can also use ``.update()`` on the container to change the label, state,
|
||||
or expanded state:
|
||||
|
||||
>>> import time
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.status("Downloading data...", expanded=True) as status:
|
||||
... st.write("Searching for data...")
|
||||
... time.sleep(2)
|
||||
... st.write("Found URL.")
|
||||
... time.sleep(1)
|
||||
... st.write("Downloading data...")
|
||||
... time.sleep(1)
|
||||
... status.update(
|
||||
... label="Download complete!", state="complete", expanded=False
|
||||
... )
|
||||
>>>
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output ::
|
||||
https://doc-status-update.streamlit.app/
|
||||
height: 300px
|
||||
|
||||
"""
|
||||
return get_dg_singleton_instance().status_container_cls._create(
|
||||
self.dg, label, expanded=expanded, state=state
|
||||
)
|
||||
|
||||
def _dialog(
|
||||
self,
|
||||
title: str,
|
||||
*,
|
||||
dismissible: bool = True,
|
||||
width: Literal["small", "large"] = "small",
|
||||
) -> Dialog:
|
||||
"""Inserts the dialog container.
|
||||
|
||||
Marked as internal because it is used by the dialog_decorator and is not supposed to be used directly.
|
||||
The dialog_decorator also has a more descriptive docstring since it is user-facing.
|
||||
"""
|
||||
return get_dg_singleton_instance().dialog_container_cls._create(
|
||||
self.dg, title, dismissible=dismissible, width=width
|
||||
)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection
|
||||
from typing import Any, Callable, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import StreamlitInvalidColorError
|
||||
|
||||
# components go from 0.0 to 1.0
|
||||
# Supported by Pillow and pretty common.
|
||||
FloatRGBColorTuple: TypeAlias = tuple[float, float, float]
|
||||
FloatRGBAColorTuple: TypeAlias = tuple[float, float, float, float]
|
||||
|
||||
# components go from 0 to 255
|
||||
# DeckGL uses these.
|
||||
IntRGBColorTuple: TypeAlias = tuple[int, int, int]
|
||||
IntRGBAColorTuple: TypeAlias = tuple[int, int, int, int]
|
||||
|
||||
# components go from 0 to 255, except alpha goes from 0.0 to 1.0
|
||||
# CSS uses these.
|
||||
MixedRGBAColorTuple: TypeAlias = tuple[int, int, int, float]
|
||||
|
||||
Color4Tuple: TypeAlias = Union[
|
||||
FloatRGBAColorTuple,
|
||||
IntRGBAColorTuple,
|
||||
MixedRGBAColorTuple,
|
||||
]
|
||||
|
||||
Color3Tuple: TypeAlias = Union[
|
||||
FloatRGBColorTuple,
|
||||
IntRGBColorTuple,
|
||||
]
|
||||
|
||||
ColorTuple: TypeAlias = Union[Color4Tuple, Color3Tuple]
|
||||
|
||||
IntColorTuple = Union[IntRGBColorTuple, IntRGBAColorTuple]
|
||||
CSSColorStr = Union[IntRGBAColorTuple, MixedRGBAColorTuple]
|
||||
|
||||
ColorStr: TypeAlias = str
|
||||
|
||||
Color: TypeAlias = Union[ColorTuple, ColorStr]
|
||||
MaybeColor: TypeAlias = Union[str, Collection[Any]]
|
||||
|
||||
|
||||
def to_int_color_tuple(color: MaybeColor) -> IntColorTuple:
|
||||
"""Convert input into color tuple of type (int, int, int, int)."""
|
||||
color_tuple = _to_color_tuple(
|
||||
color,
|
||||
rgb_formatter=_int_formatter,
|
||||
alpha_formatter=_int_formatter,
|
||||
)
|
||||
return cast("IntColorTuple", color_tuple)
|
||||
|
||||
|
||||
def to_css_color(color: MaybeColor) -> Color:
|
||||
"""Convert input into a CSS-compatible color that Vega can use.
|
||||
|
||||
Inputs must be a hex string, rgb()/rgba() string, or a color tuple. Inputs may not be a CSS
|
||||
color name, other CSS color function (like "hsl(...)"), etc.
|
||||
|
||||
See tests for more info.
|
||||
"""
|
||||
if is_css_color_like(color):
|
||||
return cast("Color", color)
|
||||
|
||||
if is_color_tuple_like(color):
|
||||
ctuple = cast("ColorTuple", color)
|
||||
ctuple = _normalize_tuple(ctuple, _int_formatter, _float_formatter)
|
||||
if len(ctuple) == 3:
|
||||
return f"rgb({ctuple[0]}, {ctuple[1]}, {ctuple[2]})"
|
||||
elif len(ctuple) == 4:
|
||||
c4tuple = cast("MixedRGBAColorTuple", ctuple)
|
||||
return f"rgba({c4tuple[0]}, {c4tuple[1]}, {c4tuple[2]}, {c4tuple[3]})"
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def is_css_color_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like something Vega can use.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
|
||||
NOTE: We only accept hex colors and color tuples as user input. So do not use this function to
|
||||
validate user input! Instead use is_hex_color_like and is_color_tuple_like.
|
||||
"""
|
||||
return is_hex_color_like(color) or _is_cssrgb_color_like(color)
|
||||
|
||||
|
||||
def is_hex_color_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like a hex color.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
"""
|
||||
return (
|
||||
isinstance(color, str)
|
||||
and color.startswith("#")
|
||||
and color[1:].isalnum() # Alphanumeric
|
||||
and len(color) in {4, 5, 7, 9}
|
||||
)
|
||||
|
||||
|
||||
def _is_cssrgb_color_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like a CSS rgb() or rgba() color string.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
|
||||
NOTE: We only accept hex colors and color tuples as user input. So do not use this function to
|
||||
validate user input! Instead use is_hex_color_like and is_color_tuple_like.
|
||||
"""
|
||||
return isinstance(color, str) and color.startswith(("rgb(", "rgba("))
|
||||
|
||||
|
||||
def is_color_tuple_like(color: MaybeColor) -> bool:
|
||||
"""Check whether the input looks like a tuple color.
|
||||
|
||||
This is meant to be lightweight, and not a definitive answer. The definitive solution is to try
|
||||
to convert and see if an error is thrown.
|
||||
"""
|
||||
return (
|
||||
isinstance(color, (tuple, list))
|
||||
and len(color) in {3, 4}
|
||||
and all(isinstance(c, (int, float)) for c in color)
|
||||
)
|
||||
|
||||
|
||||
def is_color_like(color: MaybeColor) -> bool:
|
||||
"""A fairly lightweight check of whether the input is a color.
|
||||
|
||||
This isn't meant to be a definitive answer. The definitive solution is to
|
||||
try to convert and see if an error is thrown.
|
||||
"""
|
||||
return is_css_color_like(color) or is_color_tuple_like(color)
|
||||
|
||||
|
||||
# Wrote our own hex-to-tuple parser to avoid bringing in a dependency.
|
||||
def _to_color_tuple(
|
||||
color: MaybeColor,
|
||||
rgb_formatter: Callable[[float, MaybeColor], float],
|
||||
alpha_formatter: Callable[[float, MaybeColor], float],
|
||||
):
|
||||
"""Convert a potential color to a color tuple.
|
||||
|
||||
The exact type of color tuple this outputs is dictated by the formatter parameters.
|
||||
|
||||
The R, G, B components are transformed by rgb_formatter, and the alpha component is transformed
|
||||
by alpha_formatter.
|
||||
|
||||
For example, to output a (float, float, float, int) color tuple, set rgb_formatter
|
||||
to _float_formatter and alpha_formatter to _int_formatter.
|
||||
"""
|
||||
if is_hex_color_like(color):
|
||||
hex_len = len(color)
|
||||
color_hex = cast("str", color)
|
||||
|
||||
if hex_len == 4:
|
||||
r = 2 * color_hex[1]
|
||||
g = 2 * color_hex[2]
|
||||
b = 2 * color_hex[3]
|
||||
a = "ff"
|
||||
elif hex_len == 5:
|
||||
r = 2 * color_hex[1]
|
||||
g = 2 * color_hex[2]
|
||||
b = 2 * color_hex[3]
|
||||
a = 2 * color_hex[4]
|
||||
elif hex_len == 7:
|
||||
r = color_hex[1:3]
|
||||
g = color_hex[3:5]
|
||||
b = color_hex[5:7]
|
||||
a = "ff"
|
||||
elif hex_len == 9:
|
||||
r = color_hex[1:3]
|
||||
g = color_hex[3:5]
|
||||
b = color_hex[5:7]
|
||||
a = color_hex[7:9]
|
||||
else:
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
try:
|
||||
color = int(r, 16), int(g, 16), int(b, 16), int(a, 16)
|
||||
except Exception as ex:
|
||||
raise StreamlitInvalidColorError(color) from ex
|
||||
|
||||
if is_color_tuple_like(color):
|
||||
color_tuple = cast("ColorTuple", color)
|
||||
return _normalize_tuple(color_tuple, rgb_formatter, alpha_formatter)
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def _normalize_tuple(
|
||||
color: ColorTuple,
|
||||
rgb_formatter: Callable[[float, MaybeColor], float],
|
||||
alpha_formatter: Callable[[float, MaybeColor], float],
|
||||
) -> ColorTuple:
|
||||
"""Parse color tuple using the specified color formatters.
|
||||
|
||||
The R, G, B components are transformed by rgb_formatter, and the alpha component is transformed
|
||||
by alpha_formatter.
|
||||
|
||||
For example, to output a (float, float, float, int) color tuple, set rgb_formatter
|
||||
to _float_formatter and alpha_formatter to _int_formatter.
|
||||
"""
|
||||
if len(color) == 3:
|
||||
r = rgb_formatter(color[0], color)
|
||||
g = rgb_formatter(color[1], color)
|
||||
b = rgb_formatter(color[2], color)
|
||||
return r, g, b
|
||||
|
||||
elif len(color) == 4:
|
||||
color_4tuple = cast("Color4Tuple", color)
|
||||
r = rgb_formatter(color_4tuple[0], color_4tuple)
|
||||
g = rgb_formatter(color_4tuple[1], color_4tuple)
|
||||
b = rgb_formatter(color_4tuple[2], color_4tuple)
|
||||
alpha = alpha_formatter(color_4tuple[3], color_4tuple)
|
||||
return r, g, b, alpha
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def _int_formatter(component: float, color: MaybeColor) -> int:
|
||||
"""Convert a color component (float or int) to an int from 0 to 255.
|
||||
|
||||
Anything too small will become 0, and anything too large will become 255.
|
||||
"""
|
||||
if isinstance(component, float):
|
||||
component = int(component * 255)
|
||||
|
||||
if isinstance(component, int):
|
||||
return min(255, max(component, 0))
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
|
||||
|
||||
def _float_formatter(component: float, color: MaybeColor) -> float:
|
||||
"""Convert a color component (float or int) to a float from 0.0 to 1.0.
|
||||
|
||||
Anything too small will become 0.0, and anything too large will become 1.0.
|
||||
"""
|
||||
if isinstance(component, int):
|
||||
component = component / 255.0
|
||||
|
||||
if isinstance(component, float):
|
||||
return min(1.0, max(component, 0.0))
|
||||
|
||||
raise StreamlitInvalidColorError(color)
|
||||
@@ -0,0 +1,539 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Final, Literal, Union
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.dataframe_util import DataFormat
|
||||
from streamlit.elements.lib.column_types import ColumnConfig, ColumnType
|
||||
from streamlit.elements.lib.dicttools import remove_none_values
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pyarrow as pa
|
||||
from pandas import DataFrame, Index, Series
|
||||
|
||||
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
|
||||
|
||||
|
||||
# The index identifier can be used to apply configuration options
|
||||
IndexIdentifierType = Literal["_index"]
|
||||
INDEX_IDENTIFIER: IndexIdentifierType = "_index"
|
||||
|
||||
# This is used as prefix for columns that are configured via the numerical position.
|
||||
# The integer value is converted into a string key with this prefix.
|
||||
# This needs to match with the prefix configured in the frontend.
|
||||
_NUMERICAL_POSITION_PREFIX = "_pos:"
|
||||
|
||||
|
||||
# The column data kind is used to describe the type of the data within the column.
|
||||
class ColumnDataKind(str, Enum):
|
||||
INTEGER = "integer"
|
||||
FLOAT = "float"
|
||||
DATE = "date"
|
||||
TIME = "time"
|
||||
DATETIME = "datetime"
|
||||
BOOLEAN = "boolean"
|
||||
STRING = "string"
|
||||
TIMEDELTA = "timedelta"
|
||||
PERIOD = "period"
|
||||
INTERVAL = "interval"
|
||||
BYTES = "bytes"
|
||||
DECIMAL = "decimal"
|
||||
COMPLEX = "complex"
|
||||
LIST = "list"
|
||||
DICT = "dict"
|
||||
EMPTY = "empty"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
# The dataframe schema is a mapping from the name of the column
|
||||
# in the underlying dataframe to the column data kind.
|
||||
# The index column uses `_index` as name.
|
||||
DataframeSchema: TypeAlias = dict[str, ColumnDataKind]
|
||||
|
||||
# This mapping contains all editable column types mapped to the data kinds
|
||||
# that the column type is compatible for editing.
|
||||
_EDITING_COMPATIBILITY_MAPPING: Final[dict[ColumnType, list[ColumnDataKind]]] = {
|
||||
"text": [ColumnDataKind.STRING, ColumnDataKind.EMPTY],
|
||||
"number": [
|
||||
ColumnDataKind.INTEGER,
|
||||
ColumnDataKind.FLOAT,
|
||||
ColumnDataKind.DECIMAL,
|
||||
ColumnDataKind.STRING,
|
||||
ColumnDataKind.TIMEDELTA,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"checkbox": [
|
||||
ColumnDataKind.BOOLEAN,
|
||||
ColumnDataKind.STRING,
|
||||
ColumnDataKind.INTEGER,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"selectbox": [
|
||||
ColumnDataKind.STRING,
|
||||
ColumnDataKind.BOOLEAN,
|
||||
ColumnDataKind.INTEGER,
|
||||
ColumnDataKind.FLOAT,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"date": [ColumnDataKind.DATE, ColumnDataKind.DATETIME, ColumnDataKind.EMPTY],
|
||||
"time": [ColumnDataKind.TIME, ColumnDataKind.DATETIME, ColumnDataKind.EMPTY],
|
||||
"datetime": [
|
||||
ColumnDataKind.DATETIME,
|
||||
ColumnDataKind.DATE,
|
||||
ColumnDataKind.TIME,
|
||||
ColumnDataKind.EMPTY,
|
||||
],
|
||||
"link": [ColumnDataKind.STRING, ColumnDataKind.EMPTY],
|
||||
}
|
||||
|
||||
|
||||
def is_type_compatible(column_type: ColumnType, data_kind: ColumnDataKind) -> bool:
|
||||
"""Check if the column type is compatible with the underlying data kind.
|
||||
|
||||
This check only applies to editable column types (e.g. number or text).
|
||||
Non-editable column types (e.g. bar_chart or image) can be configured for
|
||||
all data kinds (this might change in the future).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column_type : ColumnType
|
||||
The column type to check.
|
||||
|
||||
data_kind : ColumnDataKind
|
||||
The data kind to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the column type is compatible with the data kind, False otherwise.
|
||||
"""
|
||||
|
||||
if column_type not in _EDITING_COMPATIBILITY_MAPPING:
|
||||
return True
|
||||
|
||||
return data_kind in _EDITING_COMPATIBILITY_MAPPING[column_type]
|
||||
|
||||
|
||||
def _determine_data_kind_via_arrow(field: pa.Field) -> ColumnDataKind:
|
||||
"""Determine the data kind via the arrow type information.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
field : pa.Field
|
||||
The arrow field from the arrow table schema.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the field.
|
||||
"""
|
||||
import pyarrow as pa
|
||||
|
||||
field_type = field.type
|
||||
if pa.types.is_integer(field_type):
|
||||
return ColumnDataKind.INTEGER
|
||||
|
||||
if pa.types.is_floating(field_type):
|
||||
return ColumnDataKind.FLOAT
|
||||
|
||||
if pa.types.is_boolean(field_type):
|
||||
return ColumnDataKind.BOOLEAN
|
||||
|
||||
if pa.types.is_string(field_type):
|
||||
return ColumnDataKind.STRING
|
||||
|
||||
if pa.types.is_date(field_type):
|
||||
return ColumnDataKind.DATE
|
||||
|
||||
if pa.types.is_time(field_type):
|
||||
return ColumnDataKind.TIME
|
||||
|
||||
if pa.types.is_timestamp(field_type):
|
||||
return ColumnDataKind.DATETIME
|
||||
|
||||
if pa.types.is_duration(field_type):
|
||||
return ColumnDataKind.TIMEDELTA
|
||||
|
||||
if pa.types.is_list(field_type):
|
||||
return ColumnDataKind.LIST
|
||||
|
||||
if pa.types.is_decimal(field_type):
|
||||
return ColumnDataKind.DECIMAL
|
||||
|
||||
if pa.types.is_null(field_type):
|
||||
return ColumnDataKind.EMPTY
|
||||
|
||||
# Interval does not seem to work correctly:
|
||||
# if pa.types.is_interval(field_type):
|
||||
# return ColumnDataKind.INTERVAL
|
||||
|
||||
if pa.types.is_binary(field_type):
|
||||
return ColumnDataKind.BYTES
|
||||
|
||||
if pa.types.is_struct(field_type):
|
||||
return ColumnDataKind.DICT
|
||||
|
||||
return ColumnDataKind.UNKNOWN
|
||||
|
||||
|
||||
def _determine_data_kind_via_pandas_dtype(
|
||||
column: Series | Index,
|
||||
) -> ColumnDataKind:
|
||||
"""Determine the data kind by using the pandas dtype.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column : pd.Series, pd.Index
|
||||
The column for which the data kind should be determined.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the column.
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
column_dtype = column.dtype
|
||||
if pd.api.types.is_bool_dtype(column_dtype):
|
||||
return ColumnDataKind.BOOLEAN
|
||||
|
||||
if pd.api.types.is_integer_dtype(column_dtype):
|
||||
return ColumnDataKind.INTEGER
|
||||
|
||||
if pd.api.types.is_float_dtype(column_dtype):
|
||||
return ColumnDataKind.FLOAT
|
||||
|
||||
if pd.api.types.is_datetime64_any_dtype(column_dtype):
|
||||
return ColumnDataKind.DATETIME
|
||||
|
||||
if pd.api.types.is_timedelta64_dtype(column_dtype):
|
||||
return ColumnDataKind.TIMEDELTA
|
||||
|
||||
if isinstance(column_dtype, pd.PeriodDtype):
|
||||
return ColumnDataKind.PERIOD
|
||||
|
||||
if isinstance(column_dtype, pd.IntervalDtype):
|
||||
return ColumnDataKind.INTERVAL
|
||||
|
||||
if pd.api.types.is_complex_dtype(column_dtype):
|
||||
return ColumnDataKind.COMPLEX
|
||||
|
||||
if pd.api.types.is_object_dtype(
|
||||
column_dtype
|
||||
) is False and pd.api.types.is_string_dtype(column_dtype):
|
||||
# The is_string_dtype
|
||||
return ColumnDataKind.STRING
|
||||
|
||||
return ColumnDataKind.UNKNOWN
|
||||
|
||||
|
||||
def _determine_data_kind_via_inferred_type(
|
||||
column: Series | Index,
|
||||
) -> ColumnDataKind:
|
||||
"""Determine the data kind by inferring it from the underlying data.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column : pd.Series, pd.Index
|
||||
The column to determine the data kind for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the column.
|
||||
"""
|
||||
from pandas.api.types import infer_dtype
|
||||
|
||||
inferred_type = infer_dtype(column)
|
||||
|
||||
if inferred_type == "string":
|
||||
return ColumnDataKind.STRING
|
||||
|
||||
if inferred_type == "bytes":
|
||||
return ColumnDataKind.BYTES
|
||||
|
||||
if inferred_type in ["floating", "mixed-integer-float"]:
|
||||
return ColumnDataKind.FLOAT
|
||||
|
||||
if inferred_type == "integer":
|
||||
return ColumnDataKind.INTEGER
|
||||
|
||||
if inferred_type == "decimal":
|
||||
return ColumnDataKind.DECIMAL
|
||||
|
||||
if inferred_type == "complex":
|
||||
return ColumnDataKind.COMPLEX
|
||||
|
||||
if inferred_type == "boolean":
|
||||
return ColumnDataKind.BOOLEAN
|
||||
|
||||
if inferred_type in ["datetime64", "datetime"]:
|
||||
return ColumnDataKind.DATETIME
|
||||
|
||||
if inferred_type == "date":
|
||||
return ColumnDataKind.DATE
|
||||
|
||||
if inferred_type in ["timedelta64", "timedelta"]:
|
||||
return ColumnDataKind.TIMEDELTA
|
||||
|
||||
if inferred_type == "time":
|
||||
return ColumnDataKind.TIME
|
||||
|
||||
if inferred_type == "period":
|
||||
return ColumnDataKind.PERIOD
|
||||
|
||||
if inferred_type == "interval":
|
||||
return ColumnDataKind.INTERVAL
|
||||
|
||||
if inferred_type == "empty":
|
||||
return ColumnDataKind.EMPTY
|
||||
|
||||
# Unused types: mixed, unknown-array, categorical, mixed-integer
|
||||
|
||||
return ColumnDataKind.UNKNOWN
|
||||
|
||||
|
||||
def _determine_data_kind(
|
||||
column: Series | Index, field: pa.Field | None = None
|
||||
) -> ColumnDataKind:
|
||||
"""Determine the data kind of a column.
|
||||
|
||||
The column data kind refers to the shared data type of the values
|
||||
in the column (e.g. int, float, str, bool).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column : pd.Series, pd.Index
|
||||
The column to determine the data kind for.
|
||||
field : pa.Field, optional
|
||||
The arrow field from the arrow table schema.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ColumnDataKind
|
||||
The data kind of the column.
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
if isinstance(column.dtype, pd.CategoricalDtype):
|
||||
# Categorical columns can have different underlying data kinds
|
||||
# depending on the categories.
|
||||
return _determine_data_kind_via_inferred_type(column.dtype.categories)
|
||||
|
||||
if field is not None:
|
||||
data_kind = _determine_data_kind_via_arrow(field)
|
||||
if data_kind != ColumnDataKind.UNKNOWN:
|
||||
return data_kind
|
||||
|
||||
if column.dtype.name == "object":
|
||||
# If dtype is object, we need to infer the type from the column
|
||||
return _determine_data_kind_via_inferred_type(column)
|
||||
return _determine_data_kind_via_pandas_dtype(column)
|
||||
|
||||
|
||||
def determine_dataframe_schema(
|
||||
data_df: DataFrame, arrow_schema: pa.Schema
|
||||
) -> DataframeSchema:
|
||||
"""Determine the schema of a dataframe.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data_df : pd.DataFrame
|
||||
The dataframe to determine the schema of.
|
||||
arrow_schema : pa.Schema
|
||||
The Arrow schema of the dataframe.
|
||||
|
||||
Returns
|
||||
-------
|
||||
DataframeSchema
|
||||
A mapping that contains the detected data type for the index and columns.
|
||||
The key is the column name in the underlying dataframe or ``_index`` for index columns.
|
||||
"""
|
||||
|
||||
dataframe_schema: DataframeSchema = {}
|
||||
|
||||
# Add type of index:
|
||||
# TODO(lukasmasuch): We need to apply changes here to support multiindex.
|
||||
dataframe_schema[INDEX_IDENTIFIER] = _determine_data_kind(data_df.index)
|
||||
|
||||
# Add types for all columns:
|
||||
for i, column in enumerate(data_df.items()):
|
||||
column_name, column_data = column
|
||||
dataframe_schema[column_name] = _determine_data_kind(
|
||||
column_data, arrow_schema.field(i)
|
||||
)
|
||||
return dataframe_schema
|
||||
|
||||
|
||||
# A mapping of column names/IDs to column configs.
|
||||
ColumnConfigMapping: TypeAlias = dict[Union[IndexIdentifierType, str], ColumnConfig]
|
||||
ColumnConfigMappingInput: TypeAlias = Mapping[
|
||||
Union[IndexIdentifierType, str],
|
||||
Union[ColumnConfig, None, str],
|
||||
]
|
||||
|
||||
|
||||
def process_config_mapping(
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
) -> ColumnConfigMapping:
|
||||
"""Transforms a user-provided column config mapping into a valid column config mapping
|
||||
that can be used by the frontend.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column_config: dict or None
|
||||
The user-provided column config mapping.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
The transformed column config mapping.
|
||||
"""
|
||||
if column_config is None:
|
||||
return {}
|
||||
|
||||
transformed_column_config: ColumnConfigMapping = {}
|
||||
for column, config in column_config.items():
|
||||
if config is None:
|
||||
transformed_column_config[column] = ColumnConfig(hidden=True)
|
||||
elif isinstance(config, str):
|
||||
transformed_column_config[column] = ColumnConfig(label=config)
|
||||
elif isinstance(config, dict):
|
||||
# Ensure that the column config objects are cloned
|
||||
# since we will apply in-place changes to it.
|
||||
transformed_column_config[column] = copy.deepcopy(config)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid column config for column `{column}`. "
|
||||
f"Expected `None`, `str` or `dict`, but got `{type(config)}`."
|
||||
)
|
||||
return transformed_column_config
|
||||
|
||||
|
||||
def update_column_config(
|
||||
column_config_mapping: ColumnConfigMapping, column: str, column_config: ColumnConfig
|
||||
) -> None:
|
||||
"""Updates the column config value for a single column within the mapping.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
column_config_mapping : ColumnConfigMapping
|
||||
The column config mapping to update.
|
||||
|
||||
column : str
|
||||
The column to update the config value for.
|
||||
|
||||
column_config : ColumnConfig
|
||||
The column config to update.
|
||||
"""
|
||||
|
||||
if column not in column_config_mapping:
|
||||
column_config_mapping[column] = {}
|
||||
|
||||
column_config_mapping[column].update(column_config)
|
||||
|
||||
|
||||
def apply_data_specific_configs(
|
||||
columns_config: ColumnConfigMapping,
|
||||
data_format: DataFormat,
|
||||
) -> None:
|
||||
"""Apply data specific configurations to the provided dataframe.
|
||||
|
||||
This will apply inplace changes to the dataframe and the column configurations
|
||||
depending on the data format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
columns_config : ColumnConfigMapping
|
||||
A mapping of column names/ids to column configurations.
|
||||
|
||||
data_format : DataFormat
|
||||
The format of the data.
|
||||
"""
|
||||
|
||||
# Pandas adds a range index as default to all datastructures
|
||||
# but for most of the non-pandas data objects it is unnecessary
|
||||
# to show this index to the user. Therefore, we will hide it as default.
|
||||
if data_format in [
|
||||
DataFormat.SET_OF_VALUES,
|
||||
DataFormat.TUPLE_OF_VALUES,
|
||||
DataFormat.LIST_OF_VALUES,
|
||||
DataFormat.NUMPY_LIST,
|
||||
DataFormat.NUMPY_MATRIX,
|
||||
DataFormat.LIST_OF_RECORDS,
|
||||
DataFormat.LIST_OF_ROWS,
|
||||
DataFormat.COLUMN_VALUE_MAPPING,
|
||||
# Dataframe-like objects that don't have an index:
|
||||
DataFormat.PANDAS_ARRAY,
|
||||
DataFormat.PANDAS_INDEX,
|
||||
DataFormat.POLARS_DATAFRAME,
|
||||
DataFormat.POLARS_SERIES,
|
||||
DataFormat.POLARS_LAZYFRAME,
|
||||
DataFormat.PYARROW_ARRAY,
|
||||
DataFormat.RAY_DATASET,
|
||||
]:
|
||||
update_column_config(columns_config, INDEX_IDENTIFIER, {"hidden": True})
|
||||
|
||||
|
||||
def _convert_column_config_to_json(column_config_mapping: ColumnConfigMapping) -> str:
|
||||
try:
|
||||
# Ignore all None values and prefix columns specified by numerical index:
|
||||
return json.dumps(
|
||||
{
|
||||
(
|
||||
f"{_NUMERICAL_POSITION_PREFIX}{str(k)}" if isinstance(k, int) else k
|
||||
): v
|
||||
for (k, v) in remove_none_values(column_config_mapping).items()
|
||||
},
|
||||
allow_nan=False,
|
||||
)
|
||||
except ValueError as ex:
|
||||
raise StreamlitAPIException(
|
||||
f"The provided column config cannot be serialized into JSON: {ex}"
|
||||
) from ex
|
||||
|
||||
|
||||
def marshall_column_config(
|
||||
proto: ArrowProto, column_config_mapping: ColumnConfigMapping
|
||||
) -> None:
|
||||
"""Marshall the column config into the Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : ArrowProto
|
||||
The proto to marshall into.
|
||||
|
||||
column_config_mapping : ColumnConfigMapping
|
||||
The column config to marshall.
|
||||
"""
|
||||
|
||||
proto.columns = _convert_column_config_to_json(column_config_mapping)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,147 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
enqueue_message,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from streamlit.cursor import Cursor
|
||||
|
||||
DialogWidth: TypeAlias = Literal["small", "large"]
|
||||
|
||||
|
||||
def _process_dialog_width_input(
|
||||
width: DialogWidth,
|
||||
) -> BlockProto.Dialog.DialogWidth.ValueType:
|
||||
"""Maps the user-provided literal to a value of the DialogWidth proto enum.
|
||||
|
||||
Returns the mapped enum field for "small" by default and otherwise the mapped type.
|
||||
"""
|
||||
if width == "large":
|
||||
return BlockProto.Dialog.DialogWidth.LARGE
|
||||
|
||||
return BlockProto.Dialog.DialogWidth.SMALL
|
||||
|
||||
|
||||
def _assert_first_dialog_to_be_opened(should_open: bool) -> None:
|
||||
"""Check whether a dialog has already been opened in the same script run.
|
||||
|
||||
Only one dialog is supposed to be opened. The check is implemented in a way
|
||||
that for a script run, the open function can only be called once.
|
||||
One dialog at a time is a product decision and not a technical one.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException
|
||||
Raised when a dialog has already been opened in the current script run.
|
||||
"""
|
||||
script_run_ctx = get_script_run_ctx()
|
||||
# We don't reset the ctx.has_dialog_opened when the flag is False because
|
||||
# it is reset in a new scriptrun anyways. If the execution model ever changes,
|
||||
# this might need to change.
|
||||
if should_open and script_run_ctx:
|
||||
if script_run_ctx.has_dialog_opened:
|
||||
raise StreamlitAPIException(
|
||||
"Only one dialog is allowed to be opened at the same time. Please make sure to not call a dialog-decorated function more than once in a script run."
|
||||
)
|
||||
script_run_ctx.has_dialog_opened = True
|
||||
|
||||
|
||||
class Dialog(DeltaGenerator):
|
||||
@staticmethod
|
||||
def _create(
|
||||
parent: DeltaGenerator,
|
||||
title: str,
|
||||
*,
|
||||
dismissible: bool = True,
|
||||
width: DialogWidth = "small",
|
||||
) -> Dialog:
|
||||
block_proto = BlockProto()
|
||||
block_proto.dialog.title = title
|
||||
block_proto.dialog.dismissible = dismissible
|
||||
block_proto.dialog.width = _process_dialog_width_input(width)
|
||||
|
||||
# We store the delta path here, because in _update we enqueue a new proto
|
||||
# message to update the open status. Without this, the dialog content is gone
|
||||
# when the _update message is sent
|
||||
delta_path: list[int] = (
|
||||
parent._active_dg._cursor.delta_path if parent._active_dg._cursor else []
|
||||
)
|
||||
dialog = cast("Dialog", parent._block(block_proto=block_proto, dg_type=Dialog))
|
||||
|
||||
dialog._delta_path = delta_path
|
||||
dialog._current_proto = block_proto
|
||||
return dialog
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_container: int | None,
|
||||
cursor: Cursor | None,
|
||||
parent: DeltaGenerator | None,
|
||||
block_type: str | None,
|
||||
):
|
||||
super().__init__(root_container, cursor, parent, block_type)
|
||||
|
||||
# Initialized in `_create()`:
|
||||
self._current_proto: BlockProto | None = None
|
||||
self._delta_path: list[int] | None = None
|
||||
|
||||
def _update(self, should_open: bool):
|
||||
"""Send an updated proto message to indicate the open-status for the dialog."""
|
||||
|
||||
assert self._current_proto is not None, "Dialog not correctly initialized!"
|
||||
assert self._delta_path is not None, "Dialog not correctly initialized!"
|
||||
_assert_first_dialog_to_be_opened(should_open)
|
||||
msg = ForwardMsg()
|
||||
msg.metadata.delta_path[:] = self._delta_path
|
||||
msg.delta.add_block.CopyFrom(self._current_proto)
|
||||
msg.delta.add_block.dialog.is_open = should_open
|
||||
self._current_proto = msg.delta.add_block
|
||||
|
||||
enqueue_message(msg)
|
||||
|
||||
def open(self) -> None:
|
||||
self._update(True)
|
||||
|
||||
def close(self) -> None:
|
||||
self._update(False)
|
||||
|
||||
def __enter__(self) -> Self: # type: ignore[override]
|
||||
# This is a little dubious: we're returning a different type than
|
||||
# our superclass' `__enter__` function. Maybe DeltaGenerator.__enter__
|
||||
# should always return `self`?
|
||||
super().__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> Literal[False]:
|
||||
return super().__exit__(exc_type, exc_val, exc_tb)
|
||||
@@ -0,0 +1,154 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tools for working with dicts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def _unflatten_single_dict(flat_dict: dict[Any, Any]) -> dict[Any, Any]:
|
||||
"""Convert a flat dict of key-value pairs to dict tree.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
_unflatten_single_dict({
|
||||
foo_bar_baz: 123,
|
||||
foo_bar_biz: 456,
|
||||
x_bonks: 'hi',
|
||||
})
|
||||
|
||||
# Returns:
|
||||
# {
|
||||
# foo: {
|
||||
# bar: {
|
||||
# baz: 123,
|
||||
# biz: 456,
|
||||
# },
|
||||
# },
|
||||
# x: {
|
||||
# bonks: 'hi'
|
||||
# }
|
||||
# }
|
||||
|
||||
Parameters
|
||||
----------
|
||||
flat_dict : dict
|
||||
A one-level dict where keys are fully-qualified paths separated by
|
||||
underscores.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A tree made of dicts inside of dicts.
|
||||
|
||||
"""
|
||||
out: dict[str, Any] = {}
|
||||
for pathstr, v in flat_dict.items():
|
||||
path = pathstr.split("_")
|
||||
|
||||
prev_dict: dict[str, Any] | None = None
|
||||
curr_dict = out
|
||||
|
||||
for k in path:
|
||||
if k not in curr_dict:
|
||||
curr_dict[k] = {}
|
||||
prev_dict = curr_dict
|
||||
curr_dict = curr_dict[k]
|
||||
|
||||
if prev_dict is not None:
|
||||
prev_dict[k] = v
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def unflatten(
|
||||
flat_dict: dict[Any, Any], encodings: set[str] | None = None
|
||||
) -> dict[Any, Any]:
|
||||
"""Converts a flat dict of key-value pairs to a spec tree.
|
||||
|
||||
Example
|
||||
-------
|
||||
unflatten({
|
||||
foo_bar_baz: 123,
|
||||
foo_bar_biz: 456,
|
||||
x_bonks: 'hi',
|
||||
}, ['x'])
|
||||
|
||||
# Returns:
|
||||
# {
|
||||
# foo: {
|
||||
# bar: {
|
||||
# baz: 123,
|
||||
# biz: 456,
|
||||
# },
|
||||
# },
|
||||
# encoding: { # This gets added automatically
|
||||
# x: {
|
||||
# bonks: 'hi'
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
Args
|
||||
----
|
||||
flat_dict: dict
|
||||
A flat dict where keys are fully-qualified paths separated by
|
||||
underscores.
|
||||
|
||||
encodings: set
|
||||
Key names that should be automatically moved into the 'encoding' key.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A tree made of dicts inside of dicts.
|
||||
"""
|
||||
if encodings is None:
|
||||
encodings = set()
|
||||
|
||||
out_dict = _unflatten_single_dict(flat_dict)
|
||||
|
||||
for k, v in list(out_dict.items()):
|
||||
# Unflatten child dicts:
|
||||
if isinstance(v, dict):
|
||||
v = unflatten(v, encodings)
|
||||
elif hasattr(v, "__iter__"):
|
||||
for i, child in enumerate(v):
|
||||
if isinstance(child, dict):
|
||||
v[i] = unflatten(child, encodings)
|
||||
|
||||
# Move items into 'encoding' if needed:
|
||||
if k in encodings:
|
||||
if "encoding" not in out_dict:
|
||||
out_dict["encoding"] = {}
|
||||
out_dict["encoding"][k] = v
|
||||
out_dict.pop(k)
|
||||
|
||||
return out_dict
|
||||
|
||||
|
||||
def remove_none_values(input_dict: Mapping[Any, Any]) -> dict[Any, Any]:
|
||||
"""Remove all keys with None values from a dict."""
|
||||
new_dict = {}
|
||||
for key, val in input_dict.items():
|
||||
if isinstance(val, dict):
|
||||
val = remove_none_values(val)
|
||||
if val is not None:
|
||||
new_dict[key] = val
|
||||
return new_dict
|
||||
@@ -0,0 +1,37 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AttributeDictionary(dict[Any, Any]):
|
||||
"""
|
||||
A dictionary subclass that supports attribute-style access.
|
||||
|
||||
This class extends the functionality of a standard dictionary to allow items to be accessed
|
||||
via attribute-style dot notation in addition to the traditional key-based access. If a dictionary
|
||||
item is accessed and is itself a dictionary, it is automatically wrapped in another `AttributeDictionary`,
|
||||
enabling recursive attribute-style access.
|
||||
"""
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
item = self.__getitem__(key)
|
||||
return AttributeDictionary(item) if isinstance(item, dict) else item
|
||||
except KeyError as err:
|
||||
raise AttributeError(
|
||||
f"'{type(self).__name__}' object has no attribute '{key}'"
|
||||
) from err
|
||||
|
||||
__setattr__ = dict.__setitem__
|
||||
@@ -0,0 +1,66 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
TYPE_PAIRS = [
|
||||
(".jpg", ".jpeg"),
|
||||
(".mpg", ".mpeg"),
|
||||
(".mp4", ".mpeg4"),
|
||||
(".tif", ".tiff"),
|
||||
(".htm", ".html"),
|
||||
]
|
||||
|
||||
|
||||
def normalize_upload_file_type(file_type: str | Sequence[str]) -> Sequence[str]:
|
||||
if isinstance(file_type, str):
|
||||
file_type = [file_type]
|
||||
|
||||
# May need a regex or a library to validate file types are valid
|
||||
# extensions.
|
||||
file_type = [
|
||||
file_type_entry if file_type_entry[0] == "." else f".{file_type_entry}"
|
||||
for file_type_entry in file_type
|
||||
]
|
||||
|
||||
file_type = [t.lower() for t in file_type]
|
||||
|
||||
for x, y in TYPE_PAIRS:
|
||||
if x in file_type and y not in file_type:
|
||||
file_type.append(y)
|
||||
if y in file_type and x not in file_type:
|
||||
file_type.append(x)
|
||||
|
||||
return file_type
|
||||
|
||||
|
||||
def enforce_filename_restriction(filename: str, allowed_types: Sequence[str]) -> None:
|
||||
"""Ensure the uploaded file's extension matches the allowed
|
||||
types set by the app developer. In theory, this should never happen, since we
|
||||
enforce file type check by extension on the frontend, but we check it on backend
|
||||
before returning file to the user to protect ourselves.
|
||||
"""
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
if allowed_types and extension not in allowed_types:
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid file extension: `{extension}`. Allowed: {allowed_types}"
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from streamlit import runtime
|
||||
from streamlit.delta_generator_singletons import context_dg_stack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class FormData(NamedTuple):
|
||||
"""Form data stored on a DeltaGenerator."""
|
||||
|
||||
# The form's unique ID.
|
||||
form_id: str
|
||||
|
||||
|
||||
def _current_form(this_dg: DeltaGenerator) -> FormData | None:
|
||||
"""Find the FormData for the given DeltaGenerator.
|
||||
|
||||
Forms are blocks, and can have other blocks nested inside them.
|
||||
To find the current form, we walk up the dg_stack until we find
|
||||
a DeltaGenerator that has FormData.
|
||||
"""
|
||||
if not runtime.exists():
|
||||
return None
|
||||
|
||||
if this_dg._form_data is not None:
|
||||
return this_dg._form_data
|
||||
|
||||
if this_dg == this_dg._main_dg:
|
||||
# We were created via an `st.foo` call.
|
||||
# Walk up the dg_stack to see if we're nested inside a `with st.form` statement.
|
||||
for dg in reversed(context_dg_stack.get()):
|
||||
if dg._form_data is not None:
|
||||
return dg._form_data
|
||||
else:
|
||||
# We were created via an `dg.foo` call.
|
||||
# Take a look at our parent's form data to see if we're nested inside a form.
|
||||
parent = this_dg._parent
|
||||
if parent is not None and parent._form_data is not None:
|
||||
return parent._form_data
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def current_form_id(dg: DeltaGenerator) -> str:
|
||||
"""Return the form_id for the current form, or the empty string if we're
|
||||
not inside an `st.form` block.
|
||||
|
||||
(We return the empty string, instead of None, because this value is
|
||||
assigned to protobuf message fields, and None is not valid.)
|
||||
"""
|
||||
form_data = _current_form(dg)
|
||||
if form_data is None:
|
||||
return ""
|
||||
return form_data.form_id
|
||||
|
||||
|
||||
def is_in_form(dg: DeltaGenerator) -> bool:
|
||||
"""True if the DeltaGenerator is inside an st.form block."""
|
||||
return current_form_id(dg) != ""
|
||||
@@ -0,0 +1,444 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from collections.abc import Sequence
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import runtime, url_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime import caching
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import numpy.typing as npt
|
||||
from PIL import GifImagePlugin, Image, ImageFile
|
||||
|
||||
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
||||
from streamlit.type_util import NumpyShape
|
||||
|
||||
PILImage: TypeAlias = Union[
|
||||
"ImageFile.ImageFile", "Image.Image", "GifImagePlugin.GifImageFile"
|
||||
]
|
||||
AtomicImage: TypeAlias = Union[
|
||||
PILImage, "npt.NDArray[Any]", io.BytesIO, str, Path, bytes
|
||||
]
|
||||
|
||||
Channels: TypeAlias = Literal["RGB", "BGR"]
|
||||
ImageFormat: TypeAlias = Literal["JPEG", "PNG", "GIF"]
|
||||
ImageFormatOrAuto: TypeAlias = Literal[ImageFormat, "auto"]
|
||||
ImageOrImageList: TypeAlias = Union[AtomicImage, Sequence[AtomicImage]]
|
||||
|
||||
# This constant is related to the frontend maximum content width specified
|
||||
# in App.jsx main container
|
||||
# 730 is the max width of element-container in the frontend, and 2x is for high
|
||||
# DPI.
|
||||
MAXIMUM_CONTENT_WIDTH: Final[int] = 2 * 730
|
||||
|
||||
|
||||
# @see Image.proto
|
||||
# @see WidthBehavior on the frontend
|
||||
class WidthBehavior(IntEnum):
|
||||
"""
|
||||
Special values that are recognized by the frontend and allow us to change the
|
||||
behavior of the displayed image.
|
||||
"""
|
||||
|
||||
ORIGINAL = -1
|
||||
COLUMN = -2
|
||||
AUTO = -3
|
||||
MIN_IMAGE_OR_CONTAINER = -4
|
||||
MAX_IMAGE_OR_CONTAINER = -5
|
||||
|
||||
|
||||
WidthBehavior.ORIGINAL.__doc__ = """Display the image at its original width"""
|
||||
WidthBehavior.COLUMN.__doc__ = (
|
||||
"""Display the image at the width of the column it's in."""
|
||||
)
|
||||
WidthBehavior.AUTO.__doc__ = """Display the image at its original width, unless it
|
||||
would exceed the width of its column in which case clamp it to
|
||||
its column width"""
|
||||
|
||||
|
||||
def _image_may_have_alpha_channel(image: PILImage) -> bool:
|
||||
return image.mode in ("RGBA", "LA", "P")
|
||||
|
||||
|
||||
def _image_is_gif(image: PILImage) -> bool:
|
||||
return image.format == "GIF"
|
||||
|
||||
|
||||
def _validate_image_format_string(
|
||||
image_data: bytes | PILImage, format: str
|
||||
) -> ImageFormat:
|
||||
"""Return either "JPEG", "PNG", or "GIF", based on the input `format` string.
|
||||
- If `format` is "JPEG" or "JPG" (or any capitalization thereof), return "JPEG"
|
||||
- If `format` is "PNG" (or any capitalization thereof), return "PNG"
|
||||
- For all other strings, return "PNG" if the image has an alpha channel,
|
||||
"GIF" if the image is a GIF, and "JPEG" otherwise.
|
||||
"""
|
||||
format = format.upper()
|
||||
if format in {"JPEG", "PNG"}:
|
||||
return cast("ImageFormat", format)
|
||||
|
||||
# We are forgiving on the spelling of JPEG
|
||||
if format == "JPG":
|
||||
return "JPEG"
|
||||
|
||||
pil_image: PILImage
|
||||
if isinstance(image_data, bytes):
|
||||
from PIL import Image
|
||||
|
||||
pil_image = Image.open(io.BytesIO(image_data))
|
||||
else:
|
||||
pil_image = image_data
|
||||
|
||||
if _image_is_gif(pil_image):
|
||||
return "GIF"
|
||||
|
||||
if _image_may_have_alpha_channel(pil_image):
|
||||
return "PNG"
|
||||
|
||||
return "JPEG"
|
||||
|
||||
|
||||
def _PIL_to_bytes(
|
||||
image: PILImage,
|
||||
format: ImageFormat = "JPEG",
|
||||
quality: int = 100,
|
||||
) -> bytes:
|
||||
"""Convert a PIL image to bytes."""
|
||||
tmp = io.BytesIO()
|
||||
|
||||
# User must have specified JPEG, so we must convert it
|
||||
if format == "JPEG" and _image_may_have_alpha_channel(image):
|
||||
image = image.convert("RGB")
|
||||
|
||||
image.save(tmp, format=format, quality=quality)
|
||||
|
||||
return tmp.getvalue()
|
||||
|
||||
|
||||
def _BytesIO_to_bytes(data: io.BytesIO) -> bytes:
|
||||
data.seek(0)
|
||||
return data.getvalue()
|
||||
|
||||
|
||||
def _np_array_to_bytes(array: npt.NDArray[Any], output_format: str = "JPEG") -> bytes:
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
img = Image.fromarray(array.astype(np.uint8))
|
||||
format = _validate_image_format_string(img, output_format)
|
||||
|
||||
return _PIL_to_bytes(img, format)
|
||||
|
||||
|
||||
def _verify_np_shape(array: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
||||
shape: NumpyShape = array.shape
|
||||
if len(shape) not in (2, 3):
|
||||
raise StreamlitAPIException("Numpy shape has to be of length 2 or 3.")
|
||||
if len(shape) == 3 and shape[-1] not in (1, 3, 4):
|
||||
raise StreamlitAPIException(
|
||||
"Channel can only be 1, 3, or 4 got %d. Shape is %s"
|
||||
% (shape[-1], str(shape))
|
||||
)
|
||||
|
||||
# If there's only one channel, convert is to x, y
|
||||
if len(shape) == 3 and shape[-1] == 1:
|
||||
array = array[:, :, 0]
|
||||
|
||||
return array
|
||||
|
||||
|
||||
def _get_image_format_mimetype(image_format: ImageFormat) -> str:
|
||||
"""Get the mimetype string for the given ImageFormat."""
|
||||
return f"image/{image_format.lower()}"
|
||||
|
||||
|
||||
def _ensure_image_size_and_format(
|
||||
image_data: bytes, width: int, image_format: ImageFormat
|
||||
) -> bytes:
|
||||
"""Resize an image if it exceeds the given width, or if exceeds
|
||||
MAXIMUM_CONTENT_WIDTH. Ensure the image's format corresponds to the given
|
||||
ImageFormat. Return the (possibly resized and reformatted) image bytes.
|
||||
"""
|
||||
from PIL import Image
|
||||
|
||||
pil_image: PILImage = Image.open(io.BytesIO(image_data))
|
||||
actual_width, actual_height = pil_image.size
|
||||
|
||||
if width < 0 and actual_width > MAXIMUM_CONTENT_WIDTH:
|
||||
width = MAXIMUM_CONTENT_WIDTH
|
||||
|
||||
if width > 0 and actual_width > width:
|
||||
# We need to resize the image.
|
||||
new_height = int(1.0 * actual_height * width / actual_width)
|
||||
# pillow reexports Image.Resampling.BILINEAR as Image.BILINEAR for backwards
|
||||
# compatibility reasons, so we use the reexport to support older pillow
|
||||
# versions. The types don't seem to reflect this, though, hence the type: ignore
|
||||
# below.
|
||||
pil_image = pil_image.resize((width, new_height), resample=Image.BILINEAR) # type: ignore[attr-defined]
|
||||
return _PIL_to_bytes(pil_image, format=image_format, quality=90)
|
||||
|
||||
if pil_image.format != image_format:
|
||||
# We need to reformat the image.
|
||||
return _PIL_to_bytes(pil_image, format=image_format, quality=90)
|
||||
|
||||
# No resizing or reformatting necessary - return the original bytes.
|
||||
return image_data
|
||||
|
||||
|
||||
def _clip_image(image: npt.NDArray[Any], clamp: bool) -> npt.NDArray[Any]:
|
||||
import numpy as np
|
||||
|
||||
data = image
|
||||
if issubclass(image.dtype.type, np.floating):
|
||||
if clamp:
|
||||
data = np.clip(image, 0, 1.0)
|
||||
else:
|
||||
if np.amin(image) < 0.0 or np.amax(image) > 1.0:
|
||||
raise RuntimeError("Data is outside [0.0, 1.0] and clamp is not set.")
|
||||
data = data * 255
|
||||
else:
|
||||
if clamp:
|
||||
data = np.clip(image, 0, 255)
|
||||
else:
|
||||
if np.amin(image) < 0 or np.amax(image) > 255:
|
||||
raise RuntimeError("Data is outside [0, 255] and clamp is not set.")
|
||||
return data
|
||||
|
||||
|
||||
def image_to_url(
|
||||
image: AtomicImage,
|
||||
width: int,
|
||||
clamp: bool,
|
||||
channels: Channels,
|
||||
output_format: ImageFormatOrAuto,
|
||||
image_id: str,
|
||||
) -> str:
|
||||
"""Return a URL that an image can be served from.
|
||||
If `image` is already a URL, return it unmodified.
|
||||
Otherwise, add the image to the MediaFileManager and return the URL.
|
||||
(When running in "raw" mode, we won't actually load data into the
|
||||
MediaFileManager, and we'll return an empty URL).
|
||||
"""
|
||||
import numpy as np
|
||||
from PIL import Image, ImageFile
|
||||
|
||||
image_data: bytes
|
||||
|
||||
# Convert Path to string if necessary
|
||||
if isinstance(image, Path):
|
||||
image = str(image)
|
||||
|
||||
# Strings
|
||||
if isinstance(image, str):
|
||||
if not os.path.isfile(image) and url_util.is_url(
|
||||
image, allowed_schemas=("http", "https", "data")
|
||||
):
|
||||
# If it's a url, return it directly.
|
||||
return image
|
||||
|
||||
if image.endswith(".svg") and os.path.isfile(image):
|
||||
# Unpack local SVG image file to an SVG string
|
||||
with open(image) as textfile:
|
||||
image = textfile.read()
|
||||
|
||||
# Following regex allows svg image files to start either via a "<?xml...>" tag
|
||||
# eventually followed by a "<svg...>" tag or directly starting with a "<svg>" tag
|
||||
if re.search(r"(^\s?(<\?xml[\s\S]*<svg\s)|^\s?<svg\s|^\s?<svg>\s)", image):
|
||||
if "xmlns" not in image:
|
||||
# The xmlns attribute is required for SVGs to render in an img tag.
|
||||
# If it's not present, we add to the first SVG tag:
|
||||
image = image.replace(
|
||||
"<svg", '<svg xmlns="http://www.w3.org/2000/svg" ', 1
|
||||
)
|
||||
# Convert to base64 to prevent issues with encoding:
|
||||
import base64
|
||||
|
||||
image_b64_encoded = base64.b64encode(image.encode("utf-8")).decode("utf-8")
|
||||
# Return SVG as data URI:
|
||||
return f"data:image/svg+xml;base64,{image_b64_encoded}"
|
||||
|
||||
# Otherwise, try to open it as a file.
|
||||
try:
|
||||
with open(image, "rb") as f:
|
||||
image_data = f.read()
|
||||
except Exception:
|
||||
# When we aren't able to open the image file, we still pass the path to
|
||||
# the MediaFileManager - its storage backend may have access to files
|
||||
# that Streamlit does not.
|
||||
import mimetypes
|
||||
|
||||
mimetype, _ = mimetypes.guess_type(image)
|
||||
if mimetype is None:
|
||||
mimetype = "application/octet-stream"
|
||||
|
||||
url = runtime.get_instance().media_file_mgr.add(image, mimetype, image_id)
|
||||
caching.save_media_data(image, mimetype, image_id)
|
||||
return url
|
||||
|
||||
# PIL Images
|
||||
elif isinstance(image, (ImageFile.ImageFile, Image.Image)):
|
||||
format = _validate_image_format_string(image, output_format)
|
||||
image_data = _PIL_to_bytes(image, format)
|
||||
|
||||
# BytesIO
|
||||
# Note: This doesn't support SVG. We could convert to png (cairosvg.svg2png)
|
||||
# or just decode BytesIO to string and handle that way.
|
||||
elif isinstance(image, io.BytesIO):
|
||||
image_data = _BytesIO_to_bytes(image)
|
||||
|
||||
# Numpy Arrays (ie opencv)
|
||||
elif isinstance(image, np.ndarray):
|
||||
image = _clip_image(_verify_np_shape(image), clamp)
|
||||
|
||||
if channels == "BGR":
|
||||
if len(cast("NumpyShape", image.shape)) == 3:
|
||||
image = image[:, :, [2, 1, 0]]
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
'When using `channels="BGR"`, the input image should '
|
||||
"have exactly 3 color channels"
|
||||
)
|
||||
|
||||
image_data = _np_array_to_bytes(array=image, output_format=output_format)
|
||||
|
||||
# Raw bytes
|
||||
else:
|
||||
image_data = image
|
||||
|
||||
# Determine the image's format, resize it, and get its mimetype
|
||||
image_format = _validate_image_format_string(image_data, output_format)
|
||||
image_data = _ensure_image_size_and_format(image_data, width, image_format)
|
||||
mimetype = _get_image_format_mimetype(image_format)
|
||||
|
||||
if runtime.exists():
|
||||
url = runtime.get_instance().media_file_mgr.add(image_data, mimetype, image_id)
|
||||
caching.save_media_data(image_data, mimetype, image_id)
|
||||
return url
|
||||
else:
|
||||
# When running in "raw mode", we can't access the MediaFileManager.
|
||||
return ""
|
||||
|
||||
|
||||
def _4d_to_list_3d(array: npt.NDArray[Any]) -> list[npt.NDArray[Any]]:
|
||||
return [array[i, :, :, :] for i in range(array.shape[0])]
|
||||
|
||||
|
||||
def marshall_images(
|
||||
coordinates: str,
|
||||
image: ImageOrImageList,
|
||||
caption: str | npt.NDArray[Any] | list[str] | None,
|
||||
width: int | WidthBehavior,
|
||||
proto_imgs: ImageListProto,
|
||||
clamp: bool,
|
||||
channels: Channels = "RGB",
|
||||
output_format: ImageFormatOrAuto = "auto",
|
||||
) -> None:
|
||||
"""Fill an ImageListProto with a list of images and their captions.
|
||||
The images will be resized and reformatted as necessary.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates
|
||||
A string indentifying the images' location in the frontend.
|
||||
image
|
||||
The image or images to include in the ImageListProto.
|
||||
caption
|
||||
Image caption. If displaying multiple images, caption should be a
|
||||
list of captions (one for each image).
|
||||
width
|
||||
The desired width of the image or images. This parameter will be
|
||||
passed to the frontend.
|
||||
Positive values set the image width explicitly.
|
||||
Negative values has some special. For details, see: `WidthBehaviour`
|
||||
proto_imgs
|
||||
The ImageListProto to fill in.
|
||||
clamp
|
||||
Clamp image pixel values to a valid range ([0-255] per channel).
|
||||
This is only meaningful for byte array images; the parameter is
|
||||
ignored for image URLs. If this is not set, and an image has an
|
||||
out-of-range value, an error will be thrown.
|
||||
channels
|
||||
If image is an nd.array, this parameter denotes the format used to
|
||||
represent color information. Defaults to 'RGB', meaning
|
||||
`image[:, :, 0]` is the red channel, `image[:, :, 1]` is green, and
|
||||
`image[:, :, 2]` is blue. For images coming from libraries like
|
||||
OpenCV you should set this to 'BGR', instead.
|
||||
output_format
|
||||
This parameter specifies the format to use when transferring the
|
||||
image data. Photos should use the JPEG format for lossy compression
|
||||
while diagrams should use the PNG format for lossless compression.
|
||||
Defaults to 'auto' which identifies the compression type based
|
||||
on the type and format of the image argument.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
channels = cast("Channels", channels.upper())
|
||||
|
||||
# Turn single image and caption into one element list.
|
||||
images: Sequence[AtomicImage]
|
||||
if isinstance(image, (list, set, tuple)):
|
||||
images = list(image)
|
||||
elif isinstance(image, np.ndarray) and len(cast("NumpyShape", image.shape)) == 4:
|
||||
images = _4d_to_list_3d(image)
|
||||
else:
|
||||
images = cast("Sequence[AtomicImage]", [image])
|
||||
|
||||
if isinstance(caption, list):
|
||||
captions: Sequence[str | None] = caption
|
||||
elif isinstance(caption, str):
|
||||
captions = [caption]
|
||||
elif (
|
||||
isinstance(caption, np.ndarray) and len(cast("NumpyShape", caption.shape)) == 1
|
||||
):
|
||||
captions = caption.tolist()
|
||||
elif caption is None:
|
||||
captions = [None] * len(images)
|
||||
else:
|
||||
captions = [str(caption)]
|
||||
|
||||
assert isinstance(captions, list), (
|
||||
"If image is a list then caption should be as well"
|
||||
)
|
||||
assert len(captions) == len(images), "Cannot pair %d captions with %d images." % (
|
||||
len(captions),
|
||||
len(images),
|
||||
)
|
||||
|
||||
proto_imgs.width = int(width)
|
||||
# Each image in an image list needs to be kept track of at its own coordinates.
|
||||
for coord_suffix, (image, caption) in enumerate(zip(images, captions)):
|
||||
proto_img = proto_imgs.imgs.add()
|
||||
if caption is not None:
|
||||
proto_img.caption = str(caption)
|
||||
|
||||
# We use the index of the image in the input image list to identify this image inside
|
||||
# MediaFileManager. For this, we just add the index to the image's "coordinates".
|
||||
image_id = "%s-%i" % (coordinates, coord_suffix)
|
||||
|
||||
proto_img.url = image_to_url(
|
||||
image, width, clamp, channels, output_format, image_id
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numbers
|
||||
|
||||
|
||||
class JSNumberBoundsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class JSNumber:
|
||||
"""Utility class for exposing JavaScript Number constants."""
|
||||
|
||||
# The largest int that can be represented with perfect precision
|
||||
# in JavaScript.
|
||||
MAX_SAFE_INTEGER = (1 << 53) - 1
|
||||
|
||||
# The smallest int that can be represented with perfect precision
|
||||
# in JavaScript.
|
||||
MIN_SAFE_INTEGER = -((1 << 53) - 1)
|
||||
|
||||
# The largest float that can be represented in JavaScript.
|
||||
MAX_VALUE = 1.7976931348623157e308
|
||||
|
||||
# The closest number to zero that can be represented in JavaScript.
|
||||
MIN_VALUE = 5e-324
|
||||
|
||||
# The largest negative float that can be represented in JavaScript.
|
||||
MIN_NEGATIVE_VALUE = -MAX_VALUE
|
||||
|
||||
@classmethod
|
||||
def validate_int_bounds(cls, value: int, value_name: str | None = None) -> None:
|
||||
"""Validate that an int value can be represented with perfect precision
|
||||
by a JavaScript Number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : int
|
||||
value_name : str or None
|
||||
The name of the value parameter. If specified, this will be used
|
||||
in any exception that is thrown.
|
||||
|
||||
Raises
|
||||
------
|
||||
JSNumberBoundsException
|
||||
Raised with a human-readable explanation if the value falls outside
|
||||
JavaScript int bounds.
|
||||
|
||||
"""
|
||||
if value_name is None:
|
||||
value_name = "value"
|
||||
|
||||
if value < cls.MIN_SAFE_INTEGER:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be >= -((1 << 53) - 1)"
|
||||
)
|
||||
elif value > cls.MAX_SAFE_INTEGER:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be <= (1 << 53) - 1"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_float_bounds(cls, value: int | float, value_name: str | None) -> None:
|
||||
"""Validate that a float value can be represented by a JavaScript Number.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
value_name : str or None
|
||||
The name of the value parameter. If specified, this will be used
|
||||
in any exception that is thrown.
|
||||
|
||||
Raises
|
||||
------
|
||||
JSNumberBoundsException
|
||||
Raised with a human-readable explanation if the value falls outside
|
||||
JavaScript float bounds.
|
||||
|
||||
"""
|
||||
if value_name is None:
|
||||
value_name = "value"
|
||||
|
||||
if not isinstance(value, (numbers.Integral, float)):
|
||||
raise JSNumberBoundsException(f"{value_name} ({value}) is not a float")
|
||||
elif value < cls.MIN_NEGATIVE_VALUE:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be >= -1.797e+308"
|
||||
)
|
||||
elif value > cls.MAX_VALUE:
|
||||
raise JSNumberBoundsException(
|
||||
f"{value_name} ({value}) must be <= 1.797e+308"
|
||||
)
|
||||
@@ -0,0 +1,183 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import enqueue_message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types import TracebackType
|
||||
|
||||
from streamlit.cursor import Cursor
|
||||
|
||||
States: TypeAlias = Literal["running", "complete", "error"]
|
||||
|
||||
|
||||
class StatusContainer(DeltaGenerator):
|
||||
@staticmethod
|
||||
def _create(
|
||||
parent: DeltaGenerator,
|
||||
label: str,
|
||||
expanded: bool = False,
|
||||
state: States = "running",
|
||||
) -> StatusContainer:
|
||||
expandable_proto = BlockProto.Expandable()
|
||||
expandable_proto.expanded = expanded
|
||||
expandable_proto.label = label or ""
|
||||
|
||||
if state == "running":
|
||||
expandable_proto.icon = "spinner"
|
||||
elif state == "complete":
|
||||
expandable_proto.icon = ":material/check:"
|
||||
elif state == "error":
|
||||
expandable_proto.icon = ":material/error:"
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Unknown state ({state}). Must be one of 'running', 'complete', or 'error'."
|
||||
)
|
||||
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = True
|
||||
block_proto.expandable.CopyFrom(expandable_proto)
|
||||
|
||||
delta_path: list[int] = (
|
||||
parent._active_dg._cursor.delta_path if parent._active_dg._cursor else []
|
||||
)
|
||||
|
||||
status_container = cast(
|
||||
"StatusContainer",
|
||||
parent._block(block_proto=block_proto, dg_type=StatusContainer),
|
||||
)
|
||||
|
||||
# Apply initial configuration
|
||||
status_container._delta_path = delta_path
|
||||
status_container._current_proto = block_proto
|
||||
status_container._current_state = state
|
||||
|
||||
# We need to sleep here for a very short time to prevent issues when
|
||||
# the status is updated too quickly. If an .update() directly follows the
|
||||
# the initialization, sometimes only the latest update is applied.
|
||||
# Adding a short timeout here allows the frontend to render the update before.
|
||||
time.sleep(0.05)
|
||||
|
||||
return status_container
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_container: int | None,
|
||||
cursor: Cursor | None,
|
||||
parent: DeltaGenerator | None,
|
||||
block_type: str | None,
|
||||
):
|
||||
super().__init__(root_container, cursor, parent, block_type)
|
||||
|
||||
# Initialized in `_create()`:
|
||||
self._current_proto: BlockProto | None = None
|
||||
self._current_state: States | None = None
|
||||
self._delta_path: list[int] | None = None
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
label: str | None = None,
|
||||
expanded: bool | None = None,
|
||||
state: States | None = None,
|
||||
) -> None:
|
||||
"""Update the status container.
|
||||
|
||||
Only specified arguments are updated. Container contents and unspecified
|
||||
arguments remain unchanged.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str or None
|
||||
A new label of the status container. If None, the label is not
|
||||
changed.
|
||||
|
||||
expanded : bool or None
|
||||
The new expanded state of the status container. If None,
|
||||
the expanded state is not changed.
|
||||
|
||||
state : "running", "complete", "error", or None
|
||||
The new state of the status container. This mainly changes the
|
||||
icon. If None, the state is not changed.
|
||||
"""
|
||||
assert self._current_proto is not None, "Status not correctly initialized!"
|
||||
assert self._delta_path is not None, "Status not correctly initialized!"
|
||||
|
||||
msg = ForwardMsg()
|
||||
msg.metadata.delta_path[:] = self._delta_path
|
||||
msg.delta.add_block.CopyFrom(self._current_proto)
|
||||
|
||||
if expanded is not None:
|
||||
msg.delta.add_block.expandable.expanded = expanded
|
||||
else:
|
||||
msg.delta.add_block.expandable.ClearField("expanded")
|
||||
|
||||
if label is not None:
|
||||
msg.delta.add_block.expandable.label = label
|
||||
|
||||
if state is not None:
|
||||
if state == "running":
|
||||
msg.delta.add_block.expandable.icon = "spinner"
|
||||
elif state == "complete":
|
||||
msg.delta.add_block.expandable.icon = ":material/check:"
|
||||
elif state == "error":
|
||||
msg.delta.add_block.expandable.icon = ":material/error:"
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Unknown state ({state}). Must be one of 'running', 'complete', or 'error'."
|
||||
)
|
||||
self._current_state = state
|
||||
|
||||
self._current_proto = msg.delta.add_block
|
||||
enqueue_message(msg)
|
||||
|
||||
def __enter__(self) -> Self: # type: ignore[override]
|
||||
# This is a little dubious: we're returning a different type than
|
||||
# our superclass' `__enter__` function. Maybe DeltaGenerator.__enter__
|
||||
# should always return `self`?
|
||||
super().__enter__()
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_val: BaseException | None,
|
||||
exc_tb: TracebackType | None,
|
||||
) -> Literal[False]:
|
||||
# Only update if the current state is running
|
||||
if self._current_state == "running":
|
||||
# We need to sleep here for a very short time to prevent issues when
|
||||
# the status is updated too quickly. If an .update() is directly followed
|
||||
# by the exit of the context manager, sometimes only the last update
|
||||
# (to complete) is applied. Adding a short timeout here allows the frontend
|
||||
# to render the update before.
|
||||
time.sleep(0.05)
|
||||
if exc_type is not None:
|
||||
# If an exception was raised in the context,
|
||||
# we want to update the status to error.
|
||||
self.update(state="error")
|
||||
else:
|
||||
self.update(state="complete")
|
||||
return super().__exit__(exc_type, exc_val, exc_tb)
|
||||
@@ -0,0 +1,253 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, EnumMeta
|
||||
from typing import TYPE_CHECKING, Any, Final, TypeVar, overload
|
||||
|
||||
from streamlit import config, logger
|
||||
from streamlit.dataframe_util import OptionSequence, convert_anything_to_list
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.runtime.state.common import RegisterWidgetResult
|
||||
from streamlit.type_util import (
|
||||
T,
|
||||
check_python_comparable,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
|
||||
_LOGGER: Final = logger.get_logger(__name__)
|
||||
|
||||
_FLOAT_EQUALITY_EPSILON: Final[float] = 0.000000000005
|
||||
_Value = TypeVar("_Value")
|
||||
|
||||
|
||||
def index_(iterable: Iterable[_Value], x: _Value) -> int:
|
||||
"""Return zero-based index of the first item whose value is equal to x.
|
||||
Raises a ValueError if there is no such item.
|
||||
|
||||
We need a custom implementation instead of the built-in list .index() to
|
||||
be compatible with NumPy array and Pandas Series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
iterable : list, tuple, numpy.ndarray, pandas.Series
|
||||
x : Any
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
"""
|
||||
for i, value in enumerate(iterable):
|
||||
if x == value:
|
||||
return i
|
||||
elif isinstance(value, float) and isinstance(x, float):
|
||||
if abs(x - value) < _FLOAT_EQUALITY_EPSILON:
|
||||
return i
|
||||
raise ValueError(f"{str(x)} is not in iterable")
|
||||
|
||||
|
||||
def check_and_convert_to_indices(
|
||||
opt: Sequence[Any], default_values: Sequence[Any] | Any | None
|
||||
) -> list[int] | None:
|
||||
"""Perform validation checks and return indices based on the default values."""
|
||||
if default_values is None:
|
||||
return None
|
||||
|
||||
default_values = convert_anything_to_list(default_values)
|
||||
|
||||
for value in default_values:
|
||||
if value not in opt:
|
||||
raise StreamlitAPIException(
|
||||
f"The default value '{value}' is not part of the options. "
|
||||
"Please make sure that every default values also exists in the options."
|
||||
)
|
||||
|
||||
return [opt.index(value) for value in default_values]
|
||||
|
||||
|
||||
def convert_to_sequence_and_check_comparable(options: OptionSequence[T]) -> Sequence[T]:
|
||||
indexable_options = convert_anything_to_list(options)
|
||||
check_python_comparable(indexable_options)
|
||||
return indexable_options
|
||||
|
||||
|
||||
def get_default_indices(
|
||||
indexable_options: Sequence[T], default: Sequence[Any] | Any | None = None
|
||||
) -> list[int]:
|
||||
default_indices = check_and_convert_to_indices(indexable_options, default)
|
||||
default_indices = default_indices if default_indices is not None else []
|
||||
return default_indices
|
||||
|
||||
|
||||
E1 = TypeVar("E1", bound=Enum)
|
||||
E2 = TypeVar("E2", bound=Enum)
|
||||
|
||||
_ALLOWED_ENUM_COERCION_CONFIG_SETTINGS = ("off", "nameOnly", "nameAndValue")
|
||||
|
||||
|
||||
def _coerce_enum(from_enum_value: E1, to_enum_class: type[E2]) -> E1 | E2:
|
||||
"""Attempt to coerce an Enum value to another EnumMeta.
|
||||
|
||||
An Enum value of EnumMeta E1 is considered coercable to EnumType E2
|
||||
if the EnumMeta __qualname__ match and the names of their members
|
||||
match as well. (This is configurable in streamlist configs)
|
||||
"""
|
||||
if not isinstance(from_enum_value, Enum):
|
||||
raise ValueError(
|
||||
f"Expected an Enum in the first argument. Got {type(from_enum_value)}"
|
||||
)
|
||||
if not isinstance(to_enum_class, EnumMeta):
|
||||
raise ValueError(
|
||||
f"Expected an EnumMeta/Type in the second argument. Got {type(to_enum_class)}"
|
||||
)
|
||||
if isinstance(from_enum_value, to_enum_class):
|
||||
return from_enum_value # Enum is already a member, no coersion necessary
|
||||
|
||||
coercion_type = config.get_option("runner.enumCoercion")
|
||||
if coercion_type not in _ALLOWED_ENUM_COERCION_CONFIG_SETTINGS:
|
||||
raise StreamlitAPIException(
|
||||
"Invalid value for config option runner.enumCoercion. "
|
||||
f"Expected one of {_ALLOWED_ENUM_COERCION_CONFIG_SETTINGS}, "
|
||||
f"but got '{coercion_type}'."
|
||||
)
|
||||
if coercion_type == "off":
|
||||
return from_enum_value # do not attempt to coerce
|
||||
|
||||
# We now know this is an Enum AND the user has configured coercion enabled.
|
||||
# Check if we do NOT meet the required conditions and log a failure message
|
||||
# if that is the case.
|
||||
from_enum_class = from_enum_value.__class__
|
||||
if (
|
||||
from_enum_class.__qualname__ != to_enum_class.__qualname__
|
||||
or (
|
||||
coercion_type == "nameOnly"
|
||||
and set(to_enum_class._member_names_) != set(from_enum_class._member_names_)
|
||||
)
|
||||
or (
|
||||
coercion_type == "nameAndValue"
|
||||
and set(to_enum_class._value2member_map_)
|
||||
!= set(from_enum_class._value2member_map_)
|
||||
)
|
||||
):
|
||||
_LOGGER.debug("Failed to coerce %s to class %s", from_enum_value, to_enum_class)
|
||||
return from_enum_value # do not attempt to coerce
|
||||
|
||||
# At this point we think the Enum is coercable, and we know
|
||||
# E1 and E2 have the same member names. We convert from E1 to E2 using _name_
|
||||
# (since user Enum subclasses can override the .name property in 3.11)
|
||||
_LOGGER.debug("Coerced %s to class %s", from_enum_value, to_enum_class)
|
||||
return to_enum_class[from_enum_value._name_]
|
||||
|
||||
|
||||
def _extract_common_class_from_iter(iterable: Iterable[Any]) -> Any:
|
||||
"""Return the common class of all elements in a iterable if they share one.
|
||||
Otherwise, return None.
|
||||
"""
|
||||
try:
|
||||
inner_iter = iter(iterable)
|
||||
first_class = type(next(inner_iter))
|
||||
except StopIteration:
|
||||
return None
|
||||
if all(type(item) is first_class for item in inner_iter):
|
||||
return first_class
|
||||
return None
|
||||
|
||||
|
||||
@overload
|
||||
def maybe_coerce_enum(
|
||||
register_widget_result: RegisterWidgetResult[Enum],
|
||||
options: type[Enum],
|
||||
opt_sequence: Sequence[Any],
|
||||
) -> RegisterWidgetResult[Enum]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def maybe_coerce_enum(
|
||||
register_widget_result: RegisterWidgetResult[T],
|
||||
options: OptionSequence[T],
|
||||
opt_sequence: Sequence[T],
|
||||
) -> RegisterWidgetResult[T]: ...
|
||||
|
||||
|
||||
def maybe_coerce_enum(register_widget_result, options, opt_sequence):
|
||||
"""Maybe Coerce a RegisterWidgetResult with an Enum member value to
|
||||
RegisterWidgetResult[option] if option is an EnumType, otherwise just return
|
||||
the original RegisterWidgetResult.
|
||||
"""
|
||||
|
||||
# If the value is not a Enum, return early
|
||||
if not isinstance(register_widget_result.value, Enum):
|
||||
return register_widget_result
|
||||
|
||||
coerce_class: EnumMeta | None
|
||||
if isinstance(options, EnumMeta):
|
||||
coerce_class = options
|
||||
else:
|
||||
coerce_class = _extract_common_class_from_iter(opt_sequence)
|
||||
if coerce_class is None:
|
||||
return register_widget_result
|
||||
|
||||
return RegisterWidgetResult(
|
||||
_coerce_enum(register_widget_result.value, coerce_class),
|
||||
register_widget_result.value_changed,
|
||||
)
|
||||
|
||||
|
||||
# slightly ugly typing because TypeVars with Generic Bounds are not supported
|
||||
# (https://github.com/python/typing/issues/548)
|
||||
@overload
|
||||
def maybe_coerce_enum_sequence(
|
||||
register_widget_result: RegisterWidgetResult[list[T]],
|
||||
options: OptionSequence[T],
|
||||
opt_sequence: Sequence[T],
|
||||
) -> RegisterWidgetResult[list[T]]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def maybe_coerce_enum_sequence(
|
||||
register_widget_result: RegisterWidgetResult[tuple[T, T]],
|
||||
options: OptionSequence[T],
|
||||
opt_sequence: Sequence[T],
|
||||
) -> RegisterWidgetResult[tuple[T, T]]: ...
|
||||
|
||||
|
||||
def maybe_coerce_enum_sequence(register_widget_result, options, opt_sequence):
|
||||
"""Maybe Coerce a RegisterWidgetResult with a sequence of Enum members as value
|
||||
to RegisterWidgetResult[Sequence[option]] if option is an EnumType, otherwise just return
|
||||
the original RegisterWidgetResult.
|
||||
"""
|
||||
|
||||
# If not all widget values are Enums, return early
|
||||
if not all(isinstance(val, Enum) for val in register_widget_result.value):
|
||||
return register_widget_result
|
||||
|
||||
# Extract the class to coerce
|
||||
coerce_class: EnumMeta | None
|
||||
if isinstance(options, EnumMeta):
|
||||
coerce_class = options
|
||||
else:
|
||||
coerce_class = _extract_common_class_from_iter(opt_sequence)
|
||||
if coerce_class is None:
|
||||
return register_widget_result
|
||||
|
||||
# Return a new RegisterWidgetResult with the coerced enum values sequence
|
||||
return RegisterWidgetResult(
|
||||
type(register_widget_result.value)(
|
||||
_coerce_enum(val, coerce_class) for val in register_widget_result.value
|
||||
),
|
||||
register_widget_result.value_changed,
|
||||
)
|
||||
@@ -0,0 +1,274 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, TypeVar
|
||||
|
||||
from streamlit import dataframe_util
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pandas import DataFrame
|
||||
from pandas.io.formats.style import Styler
|
||||
|
||||
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
|
||||
|
||||
|
||||
def marshall_styler(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
|
||||
"""Marshall pandas.Styler into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
default_uuid : str
|
||||
If pandas.Styler uuid is not provided, this value will be used.
|
||||
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
styler_data_df: pd.DataFrame = styler.data
|
||||
if styler_data_df.size > int(pd.options.styler.render.max_elements):
|
||||
raise StreamlitAPIException(
|
||||
f"The dataframe has `{styler_data_df.size}` cells, but the maximum number "
|
||||
"of cells allowed to be rendered by Pandas Styler is configured to "
|
||||
f"`{pd.options.styler.render.max_elements}`. To allow more cells to be "
|
||||
'styled, you can change the `"styler.render.max_elements"` config. For example: '
|
||||
f'`pd.set_option("styler.render.max_elements", {styler_data_df.size})`'
|
||||
)
|
||||
|
||||
# pandas.Styler uuid should be set before _compute is called.
|
||||
_marshall_uuid(proto, styler, default_uuid)
|
||||
|
||||
# We're using protected members of pandas.Styler to get styles,
|
||||
# which is not ideal and could break if the interface changes.
|
||||
styler._compute()
|
||||
|
||||
pandas_styles = styler._translate(False, False)
|
||||
|
||||
_marshall_caption(proto, styler)
|
||||
_marshall_styles(proto, styler, pandas_styles)
|
||||
_marshall_display_values(proto, styler_data_df, pandas_styles)
|
||||
|
||||
|
||||
def _marshall_uuid(proto: ArrowProto, styler: Styler, default_uuid: str) -> None:
|
||||
"""Marshall pandas.Styler uuid into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
default_uuid : str
|
||||
If pandas.Styler uuid is not provided, this value will be used.
|
||||
|
||||
"""
|
||||
if styler.uuid is None:
|
||||
styler.set_uuid(default_uuid)
|
||||
|
||||
proto.styler.uuid = str(styler.uuid)
|
||||
|
||||
|
||||
def _marshall_caption(proto: ArrowProto, styler: Styler) -> None:
|
||||
"""Marshall pandas.Styler caption into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
"""
|
||||
if styler.caption is not None:
|
||||
proto.styler.caption = styler.caption
|
||||
|
||||
|
||||
def _marshall_styles(
|
||||
proto: ArrowProto, styler: Styler, styles: Mapping[str, Any]
|
||||
) -> None:
|
||||
"""Marshall pandas.Styler styles into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
styler : pandas.Styler
|
||||
Helps style a DataFrame or Series according to the data with HTML and CSS.
|
||||
|
||||
styles : dict
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
css_rules = []
|
||||
|
||||
if "table_styles" in styles:
|
||||
table_styles = styles["table_styles"]
|
||||
table_styles = _trim_pandas_styles(table_styles)
|
||||
for style in table_styles:
|
||||
# styles in "table_styles" have a space
|
||||
# between the uuid and selector.
|
||||
rule = _pandas_style_to_css(
|
||||
"table_styles", style, styler.uuid, separator=" "
|
||||
)
|
||||
css_rules.append(rule)
|
||||
|
||||
if "cellstyle" in styles:
|
||||
cellstyle = styles["cellstyle"]
|
||||
cellstyle = _trim_pandas_styles(cellstyle)
|
||||
for style in cellstyle:
|
||||
rule = _pandas_style_to_css("cell_style", style, styler.uuid, separator="_")
|
||||
css_rules.append(rule)
|
||||
|
||||
if len(css_rules) > 0:
|
||||
proto.styler.styles = "\n".join(css_rules)
|
||||
|
||||
|
||||
M = TypeVar("M", bound=Mapping[str, Any])
|
||||
|
||||
|
||||
def _trim_pandas_styles(styles: list[M]) -> list[M]:
|
||||
"""Filter out empty styles.
|
||||
|
||||
Every cell will have a class, but the list of props
|
||||
may just be [['', '']].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
styles : list
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
return [x for x in styles if any(any(y) for y in x["props"])]
|
||||
|
||||
|
||||
def _pandas_style_to_css(
|
||||
style_type: str,
|
||||
style: Mapping[str, Any],
|
||||
uuid: str,
|
||||
separator: str = "_",
|
||||
) -> str:
|
||||
"""Convert pandas.Styler translated style to CSS.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
style_type : str
|
||||
Either "table_styles" or "cell_style".
|
||||
|
||||
style : dict
|
||||
pandas.Styler translated style.
|
||||
|
||||
uuid : str
|
||||
pandas.Styler uuid.
|
||||
|
||||
separator : str
|
||||
A string separator used between table and cell selectors.
|
||||
|
||||
"""
|
||||
declarations = []
|
||||
for css_property, css_value in style["props"]:
|
||||
declaration = str(css_property).strip() + ": " + str(css_value).strip()
|
||||
declarations.append(declaration)
|
||||
|
||||
table_selector = f"#T_{uuid}"
|
||||
|
||||
# In pandas >= 1.1.0
|
||||
# translated_style["cellstyle"] has the following shape:
|
||||
# [
|
||||
# {
|
||||
# "props": [("color", " black"), ("background-color", "orange"), ("", "")],
|
||||
# "selectors": ["row0_col0"]
|
||||
# }
|
||||
# ...
|
||||
# ]
|
||||
if style_type == "table_styles":
|
||||
cell_selectors = [style["selector"]]
|
||||
else:
|
||||
cell_selectors = style["selectors"]
|
||||
|
||||
selectors = []
|
||||
for cell_selector in cell_selectors:
|
||||
selectors.append(table_selector + separator + cell_selector)
|
||||
selector = ", ".join(selectors)
|
||||
|
||||
declaration_block = "; ".join(declarations)
|
||||
rule_set = selector + " { " + declaration_block + " }"
|
||||
|
||||
return rule_set
|
||||
|
||||
|
||||
def _marshall_display_values(
|
||||
proto: ArrowProto, df: DataFrame, styles: Mapping[str, Any]
|
||||
) -> None:
|
||||
"""Marshall pandas.Styler display values into an Arrow proto.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
proto : proto.Arrow
|
||||
Output. The protobuf for Streamlit Arrow proto.
|
||||
|
||||
df : pandas.DataFrame
|
||||
A dataframe with original values.
|
||||
|
||||
styles : dict
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
new_df = _use_display_values(df, styles)
|
||||
proto.styler.display_values = dataframe_util.convert_pandas_df_to_arrow_bytes(
|
||||
new_df
|
||||
)
|
||||
|
||||
|
||||
def _use_display_values(df: DataFrame, styles: Mapping[str, Any]) -> DataFrame:
|
||||
"""Create a new pandas.DataFrame where display values are used instead of original ones.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pandas.DataFrame
|
||||
A dataframe with original values.
|
||||
|
||||
styles : dict
|
||||
pandas.Styler translated styles.
|
||||
|
||||
"""
|
||||
import re
|
||||
|
||||
# If values in a column are not of the same type, Arrow
|
||||
# serialization would fail. Thus, we need to cast all values
|
||||
# of the dataframe to strings before assigning them display values.
|
||||
new_df = df.astype(str)
|
||||
|
||||
cell_selector_regex = re.compile(r"row(\d+)_col(\d+)")
|
||||
if "body" in styles:
|
||||
rows = styles["body"]
|
||||
for row in rows:
|
||||
for cell in row:
|
||||
if "id" in cell:
|
||||
if match := cell_selector_regex.match(cell["id"]):
|
||||
r, c = map(int, match.groups())
|
||||
new_df.iloc[r, c] = str(cell["display_value"])
|
||||
|
||||
return new_df
|
||||
@@ -0,0 +1,194 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from streamlit import config, errors, logger, runtime
|
||||
from streamlit.elements.lib.form_utils import is_in_form
|
||||
from streamlit.errors import (
|
||||
StreamlitAPIWarning,
|
||||
StreamlitFragmentWidgetsNotAllowedOutsideError,
|
||||
StreamlitInvalidFormCallbackError,
|
||||
StreamlitValueAssignmentNotAllowedError,
|
||||
)
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
get_script_run_ctx,
|
||||
in_cached_function,
|
||||
)
|
||||
from streamlit.runtime.state import WidgetCallback, get_session_state
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
_LOGGER: Final = logger.get_logger(__name__)
|
||||
|
||||
|
||||
def check_callback_rules(dg: DeltaGenerator, on_change: WidgetCallback | None) -> None:
|
||||
"""Ensures that widgets other than `st.form_submit_button` within a form don't have
|
||||
an on_change callback set.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitInvalidFormCallbackError:
|
||||
Raised when the described rule is violated.
|
||||
"""
|
||||
|
||||
if runtime.exists() and is_in_form(dg) and on_change is not None:
|
||||
raise StreamlitInvalidFormCallbackError()
|
||||
|
||||
|
||||
_shown_default_value_warning: bool = False
|
||||
|
||||
|
||||
def check_session_state_rules(
|
||||
default_value: Any, key: str | None, writes_allowed: bool = True
|
||||
) -> None:
|
||||
"""Ensures that no values are set for widgets with the given key when writing
|
||||
is not allowed.
|
||||
|
||||
Additionally, if `global.disableWidgetStateDuplicationWarning` is False a warning is
|
||||
shown when a widget has a default value but its value is also set via session state.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException:
|
||||
Raised when the described rule is violated.
|
||||
"""
|
||||
global _shown_default_value_warning
|
||||
|
||||
if key is None or not runtime.exists():
|
||||
return
|
||||
|
||||
session_state = get_session_state()
|
||||
if not session_state.is_new_state_value(key):
|
||||
return
|
||||
|
||||
if not writes_allowed:
|
||||
raise StreamlitValueAssignmentNotAllowedError(key=key)
|
||||
|
||||
if (
|
||||
default_value is not None
|
||||
and not _shown_default_value_warning
|
||||
and not config.get_option("global.disableWidgetStateDuplicationWarning")
|
||||
):
|
||||
from streamlit import warning
|
||||
|
||||
warning(
|
||||
f'The widget with key "{key}" was created with a default value but'
|
||||
" also had its value set via the Session State API."
|
||||
)
|
||||
_shown_default_value_warning = True
|
||||
|
||||
|
||||
class CachedWidgetWarning(StreamlitAPIWarning):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"""
|
||||
Your script uses a widget command in a cached function
|
||||
(function decorated with `@st.cache_data` or `@st.cache_resource`).
|
||||
This code will only be called when we detect a cache "miss",
|
||||
which can lead to unexpected results.
|
||||
|
||||
To fix this, move all widget commands outside the cached function.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def check_cache_replay_rules() -> None:
|
||||
"""Check if a widget is allowed to be used in the current context.
|
||||
More specifically, this checks if the current context is inside a
|
||||
cached function that disallows widget usage. If so, it raises a warning.
|
||||
|
||||
If there are other similar checks in the future, we could extend this
|
||||
function to check for those as well. And rename it to check_widget_usage_rules.
|
||||
"""
|
||||
if in_cached_function.get():
|
||||
from streamlit import exception
|
||||
|
||||
# We use an exception here to show a proper stack trace
|
||||
# that indicates to the user where the issue is.
|
||||
exception(CachedWidgetWarning())
|
||||
|
||||
|
||||
def check_fragment_path_policy(dg: DeltaGenerator):
|
||||
"""Ensures that the current widget is not written outside of the
|
||||
fragment's delta path.
|
||||
|
||||
Should be called by ever element that acts as a widget.
|
||||
We don't allow writing widgets from within a widget to the outside path
|
||||
because it can lead to unexpected behavior. For elements, this is okay
|
||||
because they do not trigger a re-run.
|
||||
"""
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
# Check is only relevant for fragments
|
||||
if ctx is None or ctx.current_fragment_id is None:
|
||||
return
|
||||
|
||||
current_fragment_delta_path = ctx.current_fragment_delta_path
|
||||
current_cursor = dg._active_dg._cursor
|
||||
if current_cursor is None:
|
||||
return
|
||||
|
||||
current_cursor_delta_path = current_cursor.delta_path
|
||||
|
||||
# the elements delta path cannot be smaller than the fragment's delta path if it is
|
||||
# inside of the fragment
|
||||
if len(current_cursor_delta_path) < len(current_fragment_delta_path):
|
||||
raise StreamlitFragmentWidgetsNotAllowedOutsideError()
|
||||
|
||||
# all path indices of the fragment-path must occur in the inner-elements delta path,
|
||||
# otherwise it is outside of the fragment container
|
||||
for index, path_index in enumerate(current_fragment_delta_path):
|
||||
if current_cursor_delta_path[index] != path_index:
|
||||
raise StreamlitFragmentWidgetsNotAllowedOutsideError()
|
||||
|
||||
|
||||
def check_widget_policies(
|
||||
dg: DeltaGenerator,
|
||||
key: str | None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
*,
|
||||
default_value: Sequence[Any] | Any | None = None,
|
||||
writes_allowed: bool = True,
|
||||
enable_check_callback_rules: bool = True,
|
||||
):
|
||||
"""Check all widget policies for the given DeltaGenerator."""
|
||||
check_fragment_path_policy(dg)
|
||||
check_cache_replay_rules()
|
||||
if enable_check_callback_rules:
|
||||
check_callback_rules(dg, on_change)
|
||||
check_session_state_rules(
|
||||
default_value=default_value, key=key, writes_allowed=writes_allowed
|
||||
)
|
||||
|
||||
|
||||
def maybe_raise_label_warnings(label: str | None, label_visibility: str | None):
|
||||
if not label:
|
||||
_LOGGER.warning(
|
||||
"`label` got an empty value. This is discouraged for accessibility "
|
||||
"reasons and may be disallowed in the future by raising an exception. "
|
||||
"Please provide a non-empty label and hide it with label_visibility "
|
||||
"if needed."
|
||||
)
|
||||
if label_visibility not in ("visible", "hidden", "collapsed"):
|
||||
raise errors.StreamlitAPIException(
|
||||
f"Unsupported label_visibility option '{label_visibility}'. "
|
||||
f"Valid values are 'visible', 'hidden' or 'collapsed'."
|
||||
)
|
||||
@@ -0,0 +1,207 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
|
||||
|
||||
def configure_streamlit_plotly_theme() -> None:
|
||||
"""Configure the Streamlit chart theme for Plotly.
|
||||
|
||||
The theme is only configured if Plotly is installed.
|
||||
"""
|
||||
# We do nothing if Plotly is not installed. This is expected since Plotly is an optional dependency.
|
||||
with contextlib.suppress(ImportError):
|
||||
import plotly.graph_objects as go
|
||||
import plotly.io as pio
|
||||
|
||||
# This is the streamlit theme for plotly where we pass in a template.data
|
||||
# and a template.layout.
|
||||
# Template.data is for changing specific graph properties in a general aspect
|
||||
# such as Contour plots or Waterfall plots.
|
||||
# Template.layout is for changing things such as the x axis and fonts and other
|
||||
# general layout properties for general graphs.
|
||||
# We pass in temporary colors to the frontend and the frontend will replace
|
||||
# those colors because we want to change colors based on the background color.
|
||||
# Start at #0000001 because developers may be likely to use #000000
|
||||
CATEGORY_0 = "#000001"
|
||||
CATEGORY_1 = "#000002"
|
||||
CATEGORY_2 = "#000003"
|
||||
CATEGORY_3 = "#000004"
|
||||
CATEGORY_4 = "#000005"
|
||||
CATEGORY_5 = "#000006"
|
||||
CATEGORY_6 = "#000007"
|
||||
CATEGORY_7 = "#000008"
|
||||
CATEGORY_8 = "#000009"
|
||||
CATEGORY_9 = "#000010"
|
||||
|
||||
SEQUENTIAL_0 = "#000011"
|
||||
SEQUENTIAL_1 = "#000012"
|
||||
SEQUENTIAL_2 = "#000013"
|
||||
SEQUENTIAL_3 = "#000014"
|
||||
SEQUENTIAL_4 = "#000015"
|
||||
SEQUENTIAL_5 = "#000016"
|
||||
SEQUENTIAL_6 = "#000017"
|
||||
SEQUENTIAL_7 = "#000018"
|
||||
SEQUENTIAL_8 = "#000019"
|
||||
SEQUENTIAL_9 = "#000020"
|
||||
|
||||
DIVERGING_0 = "#000021"
|
||||
DIVERGING_1 = "#000022"
|
||||
DIVERGING_2 = "#000023"
|
||||
DIVERGING_3 = "#000024"
|
||||
DIVERGING_4 = "#000025"
|
||||
DIVERGING_5 = "#000026"
|
||||
DIVERGING_6 = "#000027"
|
||||
DIVERGING_7 = "#000028"
|
||||
DIVERGING_8 = "#000029"
|
||||
DIVERGING_9 = "#000030"
|
||||
DIVERGING_10 = "#000031"
|
||||
|
||||
INCREASING = "#000032"
|
||||
DECREASING = "#000033"
|
||||
TOTAL = "#000034"
|
||||
|
||||
GRAY_70 = "#000036"
|
||||
GRAY_90 = "#000037"
|
||||
BG_COLOR = "#000038"
|
||||
FADED_TEXT_05 = "#000039"
|
||||
BG_MIX = "#000040"
|
||||
|
||||
# Plotly represents continuous colorscale through an array of pairs.
|
||||
# The pair's first index is the starting point and the next pair's first index is the end point.
|
||||
# The pair's second index is the starting color and the next pair's second index is the end color.
|
||||
# For more information, please refer to https://plotly.com/python/colorscales/
|
||||
|
||||
streamlit_colorscale = [
|
||||
[0.0, SEQUENTIAL_0],
|
||||
[0.1111111111111111, SEQUENTIAL_1],
|
||||
[0.2222222222222222, SEQUENTIAL_2],
|
||||
[0.3333333333333333, SEQUENTIAL_3],
|
||||
[0.4444444444444444, SEQUENTIAL_4],
|
||||
[0.5555555555555556, SEQUENTIAL_5],
|
||||
[0.6666666666666666, SEQUENTIAL_6],
|
||||
[0.7777777777777778, SEQUENTIAL_7],
|
||||
[0.8888888888888888, SEQUENTIAL_8],
|
||||
[1.0, SEQUENTIAL_9],
|
||||
]
|
||||
|
||||
pio.templates["streamlit"] = go.layout.Template(
|
||||
data=go.layout.template.Data(
|
||||
candlestick=[
|
||||
go.layout.template.data.Candlestick(
|
||||
decreasing=go.candlestick.Decreasing(
|
||||
line=go.candlestick.decreasing.Line(color=DECREASING)
|
||||
),
|
||||
increasing=go.candlestick.Increasing(
|
||||
line=go.candlestick.increasing.Line(color=INCREASING)
|
||||
),
|
||||
)
|
||||
],
|
||||
contour=[
|
||||
go.layout.template.data.Contour(colorscale=streamlit_colorscale)
|
||||
],
|
||||
contourcarpet=[
|
||||
go.layout.template.data.Contourcarpet(
|
||||
colorscale=streamlit_colorscale
|
||||
)
|
||||
],
|
||||
heatmap=[
|
||||
go.layout.template.data.Heatmap(colorscale=streamlit_colorscale)
|
||||
],
|
||||
histogram2d=[
|
||||
go.layout.template.data.Histogram2d(colorscale=streamlit_colorscale)
|
||||
],
|
||||
icicle=[
|
||||
go.layout.template.data.Icicle(
|
||||
textfont=go.icicle.Textfont(color="white")
|
||||
)
|
||||
],
|
||||
sankey=[
|
||||
go.layout.template.data.Sankey(
|
||||
textfont=go.sankey.Textfont(color=GRAY_70)
|
||||
)
|
||||
],
|
||||
scatter=[
|
||||
go.layout.template.data.Scatter(
|
||||
marker=go.scatter.Marker(line=go.scatter.marker.Line(width=0))
|
||||
)
|
||||
],
|
||||
table=[
|
||||
go.layout.template.data.Table(
|
||||
cells=go.table.Cells(
|
||||
fill=go.table.cells.Fill(color=BG_COLOR),
|
||||
font=go.table.cells.Font(color=GRAY_90),
|
||||
line=go.table.cells.Line(color=FADED_TEXT_05),
|
||||
),
|
||||
header=go.table.Header(
|
||||
font=go.table.header.Font(color=GRAY_70),
|
||||
line=go.table.header.Line(color=FADED_TEXT_05),
|
||||
fill=go.table.header.Fill(color=BG_MIX),
|
||||
),
|
||||
)
|
||||
],
|
||||
waterfall=[
|
||||
go.layout.template.data.Waterfall(
|
||||
increasing=go.waterfall.Increasing(
|
||||
marker=go.waterfall.increasing.Marker(color=INCREASING)
|
||||
),
|
||||
decreasing=go.waterfall.Decreasing(
|
||||
marker=go.waterfall.decreasing.Marker(color=DECREASING)
|
||||
),
|
||||
totals=go.waterfall.Totals(
|
||||
marker=go.waterfall.totals.Marker(color=TOTAL)
|
||||
),
|
||||
connector=go.waterfall.Connector(
|
||||
line=go.waterfall.connector.Line(color=GRAY_70, width=2)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
layout=go.Layout(
|
||||
colorway=[
|
||||
CATEGORY_0,
|
||||
CATEGORY_1,
|
||||
CATEGORY_2,
|
||||
CATEGORY_3,
|
||||
CATEGORY_4,
|
||||
CATEGORY_5,
|
||||
CATEGORY_6,
|
||||
CATEGORY_7,
|
||||
CATEGORY_8,
|
||||
CATEGORY_9,
|
||||
],
|
||||
colorscale=go.layout.Colorscale(
|
||||
sequential=streamlit_colorscale,
|
||||
sequentialminus=streamlit_colorscale,
|
||||
diverging=[
|
||||
[0.0, DIVERGING_0],
|
||||
[0.1, DIVERGING_1],
|
||||
[0.2, DIVERGING_2],
|
||||
[0.3, DIVERGING_3],
|
||||
[0.4, DIVERGING_4],
|
||||
[0.5, DIVERGING_5],
|
||||
[0.6, DIVERGING_6],
|
||||
[0.7, DIVERGING_7],
|
||||
[0.8, DIVERGING_8],
|
||||
[0.9, DIVERGING_9],
|
||||
[1.0, DIVERGING_10],
|
||||
],
|
||||
),
|
||||
coloraxis=go.layout.Coloraxis(colorscale=streamlit_colorscale),
|
||||
),
|
||||
)
|
||||
|
||||
pio.templates.default = "streamlit"
|
||||
@@ -0,0 +1,178 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from streamlit import runtime
|
||||
from streamlit.runtime import caching
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
# Regular expression to match the SRT timestamp format
|
||||
# It matches the
|
||||
# "hours:minutes:seconds,milliseconds --> hours:minutes:seconds,milliseconds" format
|
||||
SRT_VALIDATION_REGEX = r"\d{2}:\d{2}:\d{2},\d{3} --> \d{2}:\d{2}:\d{2},\d{3}"
|
||||
|
||||
SRT_CONVERSION_REGEX = r"(\d{2}:\d{2}:\d{2}),(\d{3})"
|
||||
|
||||
SUBTITLE_ALLOWED_FORMATS = (".srt", ".vtt")
|
||||
|
||||
|
||||
def _is_srt(stream: str | io.BytesIO | bytes) -> bool:
|
||||
# Handle raw bytes
|
||||
if isinstance(stream, bytes):
|
||||
stream = io.BytesIO(stream)
|
||||
|
||||
# Convert str to io.BytesIO if 'stream' is a string
|
||||
if isinstance(stream, str):
|
||||
stream = io.BytesIO(stream.encode("utf-8"))
|
||||
|
||||
# Set the stream position to the beginning in case it's been moved
|
||||
stream.seek(0)
|
||||
|
||||
# Read enough bytes to reliably check for SRT patterns
|
||||
# This might be adjusted, but 33 bytes should be enough to read the first numeric
|
||||
# line, the full timestamp line, and a bit of the next line
|
||||
header = stream.read(33)
|
||||
|
||||
try:
|
||||
header_str = header.decode("utf-8").strip() # Decode and strip whitespace
|
||||
except UnicodeDecodeError:
|
||||
# If it's not valid utf-8, it's probably not a valid SRT file
|
||||
return False
|
||||
|
||||
# Split the header into lines and process them
|
||||
lines = header_str.split("\n")
|
||||
|
||||
# Check for the pattern of an SRT file: digit(s), newline, timestamp
|
||||
if len(lines) >= 2 and lines[0].isdigit():
|
||||
match = re.search(SRT_VALIDATION_REGEX, lines[1])
|
||||
if match:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _srt_to_vtt(srt_data: str | bytes) -> bytes:
|
||||
"""
|
||||
Convert subtitles from SubRip (.srt) format to WebVTT (.vtt) format.
|
||||
This function accepts the content of the .srt file either as a string
|
||||
or as a BytesIO stream.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
srt_data : str or bytes
|
||||
The content of the .srt file as a string or a bytes stream.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The content converted into .vtt format.
|
||||
"""
|
||||
|
||||
# If the input is a bytes stream, convert it to a string
|
||||
if isinstance(srt_data, bytes):
|
||||
# Decode the bytes to a UTF-8 string
|
||||
try:
|
||||
srt_data = srt_data.decode("utf-8")
|
||||
except UnicodeDecodeError as e:
|
||||
raise ValueError("Could not decode the input stream as UTF-8.") from e
|
||||
if not isinstance(srt_data, str):
|
||||
# If it's not a string by this point, something is wrong.
|
||||
raise TypeError(
|
||||
f"Input must be a string or a bytes stream, not {type(srt_data)}."
|
||||
)
|
||||
|
||||
# Replace SubRip timing with WebVTT timing
|
||||
vtt_data = re.sub(SRT_CONVERSION_REGEX, r"\1.\2", srt_data)
|
||||
|
||||
# Add WebVTT file header
|
||||
vtt_content = "WEBVTT\n\n" + vtt_data
|
||||
# Convert the vtt content to bytes
|
||||
vtt_content = vtt_content.strip().encode("utf-8")
|
||||
|
||||
return vtt_content
|
||||
|
||||
|
||||
def _handle_string_or_path_data(data_or_path: str | Path) -> bytes:
|
||||
"""Handles string data, either as a file path or raw content."""
|
||||
if os.path.isfile(data_or_path):
|
||||
path = Path(data_or_path)
|
||||
file_extension = path.suffix.lower()
|
||||
|
||||
if file_extension not in SUBTITLE_ALLOWED_FORMATS:
|
||||
raise ValueError(
|
||||
f"Incorrect subtitle format {file_extension}. Subtitles must be in "
|
||||
f"one of the following formats: {', '.join(SUBTITLE_ALLOWED_FORMATS)}"
|
||||
)
|
||||
with open(data_or_path, "rb") as file:
|
||||
content = file.read()
|
||||
return _srt_to_vtt(content) if file_extension == ".srt" else content
|
||||
elif isinstance(data_or_path, Path):
|
||||
raise ValueError(f"File {data_or_path} does not exist.")
|
||||
|
||||
content_string = data_or_path.strip()
|
||||
|
||||
if content_string.startswith("WEBVTT") or content_string == "":
|
||||
return content_string.encode("utf-8")
|
||||
elif _is_srt(content_string):
|
||||
return _srt_to_vtt(content_string)
|
||||
raise ValueError("The provided string neither matches valid VTT nor SRT format.")
|
||||
|
||||
|
||||
def _handle_stream_data(stream: io.BytesIO) -> bytes:
|
||||
"""Handles io.BytesIO data, converting SRT to VTT content if needed."""
|
||||
stream.seek(0)
|
||||
stream_data = stream.getvalue()
|
||||
return _srt_to_vtt(stream_data) if _is_srt(stream) else stream_data
|
||||
|
||||
|
||||
def _handle_bytes_data(data: bytes) -> bytes:
|
||||
"""Handles io.BytesIO data, converting SRT to VTT content if needed."""
|
||||
return _srt_to_vtt(data) if _is_srt(data) else data
|
||||
|
||||
|
||||
def process_subtitle_data(
|
||||
coordinates: str,
|
||||
data: str | bytes | Path | io.BytesIO,
|
||||
label: str,
|
||||
) -> str:
|
||||
# Determine the type of data and process accordingly
|
||||
if isinstance(data, (str, Path)):
|
||||
subtitle_data = _handle_string_or_path_data(data)
|
||||
elif isinstance(data, io.BytesIO):
|
||||
subtitle_data = _handle_stream_data(data)
|
||||
elif isinstance(data, bytes):
|
||||
subtitle_data = _handle_bytes_data(data)
|
||||
else:
|
||||
raise TypeError(f"Invalid binary data format for subtitle: {type(data)}.")
|
||||
|
||||
if runtime.exists():
|
||||
filename = calc_md5(label.encode())
|
||||
# Save the processed data and return the file URL
|
||||
file_url = runtime.get_instance().media_file_mgr.add(
|
||||
path_or_data=subtitle_data,
|
||||
mimetype="text/vtt",
|
||||
coordinates=coordinates,
|
||||
file_name=f"{filename}.vtt",
|
||||
)
|
||||
caching.save_media_data(subtitle_data, "text/vtt", coordinates)
|
||||
return file_url
|
||||
else:
|
||||
# When running in "raw mode", we can't access the MediaFileManager.
|
||||
return ""
|
||||
@@ -0,0 +1,248 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Union,
|
||||
overload,
|
||||
)
|
||||
|
||||
from google.protobuf.message import Message
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.errors import StreamlitDuplicateElementId, StreamlitDuplicateElementKey
|
||||
from streamlit.proto.ChatInput_pb2 import ChatInput
|
||||
from streamlit.proto.LabelVisibilityMessage_pb2 import LabelVisibilityMessage
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
||||
ScriptRunContext,
|
||||
get_script_run_ctx,
|
||||
)
|
||||
from streamlit.runtime.state.common import (
|
||||
GENERATED_ELEMENT_ID_PREFIX,
|
||||
TESTING_KEY,
|
||||
user_key_from_element_id,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from builtins import ellipsis
|
||||
from collections.abc import Iterable
|
||||
|
||||
|
||||
Key: TypeAlias = Union[str, int]
|
||||
|
||||
LabelVisibility: TypeAlias = Literal["visible", "hidden", "collapsed"]
|
||||
|
||||
PROTO_SCALAR_VALUE = Union[float, int, bool, str, bytes]
|
||||
SAFE_VALUES = Union[
|
||||
date,
|
||||
time,
|
||||
datetime,
|
||||
timedelta,
|
||||
None,
|
||||
"ellipsis",
|
||||
Message,
|
||||
PROTO_SCALAR_VALUE,
|
||||
]
|
||||
|
||||
|
||||
def get_label_visibility_proto_value(
|
||||
label_visibility_string: LabelVisibility,
|
||||
) -> LabelVisibilityMessage.LabelVisibilityOptions.ValueType:
|
||||
"""Returns one of LabelVisibilityMessage enum constants.py based on string value."""
|
||||
|
||||
if label_visibility_string == "visible":
|
||||
return LabelVisibilityMessage.LabelVisibilityOptions.VISIBLE
|
||||
elif label_visibility_string == "hidden":
|
||||
return LabelVisibilityMessage.LabelVisibilityOptions.HIDDEN
|
||||
elif label_visibility_string == "collapsed":
|
||||
return LabelVisibilityMessage.LabelVisibilityOptions.COLLAPSED
|
||||
|
||||
raise ValueError(f"Unknown label visibility value: {label_visibility_string}")
|
||||
|
||||
|
||||
def get_chat_input_accept_file_proto_value(
|
||||
accept_file_value: bool | Literal["multiple"],
|
||||
) -> ChatInput.AcceptFile.ValueType:
|
||||
"""Returns one of ChatInput.AcceptFile enum value based on string value."""
|
||||
|
||||
if accept_file_value is False:
|
||||
return ChatInput.AcceptFile.NONE
|
||||
elif accept_file_value is True:
|
||||
return ChatInput.AcceptFile.SINGLE
|
||||
elif accept_file_value == "multiple":
|
||||
return ChatInput.AcceptFile.MULTIPLE
|
||||
|
||||
raise ValueError(f"Unknown accept file value: {accept_file_value}")
|
||||
|
||||
|
||||
@overload
|
||||
def to_key(key: None) -> None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def to_key(key: Key) -> str: ...
|
||||
|
||||
|
||||
def to_key(key: Key | None) -> str | None:
|
||||
return None if key is None else str(key)
|
||||
|
||||
|
||||
def _register_element_id(
|
||||
ctx: ScriptRunContext, element_type: str, element_id: str
|
||||
) -> None:
|
||||
"""Register the element ID and key for the given element.
|
||||
|
||||
If the element ID or key is not unique, an error is raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_type : str
|
||||
The type of the element to register.
|
||||
|
||||
element_id : str
|
||||
The ID of the element to register.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitDuplicateElementKey
|
||||
If the element key is not unique.
|
||||
|
||||
StreamlitDuplicateElementID
|
||||
If the element ID is not unique.
|
||||
|
||||
"""
|
||||
|
||||
if not element_id:
|
||||
return
|
||||
|
||||
if user_key := user_key_from_element_id(element_id):
|
||||
if user_key not in ctx.widget_user_keys_this_run:
|
||||
ctx.widget_user_keys_this_run.add(user_key)
|
||||
else:
|
||||
raise StreamlitDuplicateElementKey(user_key)
|
||||
|
||||
if element_id not in ctx.widget_ids_this_run:
|
||||
ctx.widget_ids_this_run.add(element_id)
|
||||
else:
|
||||
raise StreamlitDuplicateElementId(element_type)
|
||||
|
||||
|
||||
def _compute_element_id(
|
||||
element_type: str,
|
||||
user_key: str | None = None,
|
||||
**kwargs: SAFE_VALUES | Iterable[SAFE_VALUES],
|
||||
) -> str:
|
||||
"""Compute the ID for the given element.
|
||||
|
||||
This ID is stable: a given set of inputs to this function will always produce
|
||||
the same ID output. Only stable, deterministic values should be used to compute
|
||||
element IDs. Using nondeterministic values as inputs can cause the resulting
|
||||
element ID to change between runs.
|
||||
|
||||
The element ID includes the user_key so elements with identical arguments can
|
||||
use it to be distinct. The element ID includes an easily identified prefix, and the
|
||||
user_key as a suffix, to make it easy to identify it and know if a key maps to it.
|
||||
"""
|
||||
h = hashlib.new("md5", usedforsecurity=False)
|
||||
h.update(element_type.encode("utf-8"))
|
||||
if user_key:
|
||||
# Adding this to the hash isn't necessary for uniqueness since the
|
||||
# key is also appended to the ID as raw text. But since the hash and
|
||||
# the appending of the key are two slightly different aspects, it
|
||||
# still gets put into the hash.
|
||||
h.update(user_key.encode("utf-8"))
|
||||
# This will iterate in a consistent order when the provided arguments have
|
||||
# consistent order; dicts are always in insertion order.
|
||||
for k, v in kwargs.items():
|
||||
h.update(str(k).encode("utf-8"))
|
||||
h.update(str(v).encode("utf-8"))
|
||||
return f"{GENERATED_ELEMENT_ID_PREFIX}-{h.hexdigest()}-{user_key}"
|
||||
|
||||
|
||||
def compute_and_register_element_id(
|
||||
element_type: str,
|
||||
*,
|
||||
user_key: str | None,
|
||||
form_id: str | None,
|
||||
**kwargs: SAFE_VALUES | Iterable[SAFE_VALUES],
|
||||
) -> str:
|
||||
"""Compute and register the ID for the given element.
|
||||
|
||||
This ID is stable: a given set of inputs to this function will always produce
|
||||
the same ID output. Only stable, deterministic values should be used to compute
|
||||
element IDs. Using nondeterministic values as inputs can cause the resulting
|
||||
element ID to change between runs.
|
||||
|
||||
The element ID includes the user_key so elements with identical arguments can
|
||||
use it to be distinct. The element ID includes an easily identified prefix, and the
|
||||
user_key as a suffix, to make it easy to identify it and know if a key maps to it.
|
||||
|
||||
The element ID gets registered to make sure that only one ID and user-specified
|
||||
key exists at the same time. If there are duplicated IDs or keys, an error
|
||||
is raised.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
element_type : str
|
||||
The type (command name) of the element to register.
|
||||
|
||||
user_key : str | None
|
||||
The user-specified key for the element. `None` if no key is provided
|
||||
or if the element doesn't support a specifying a key.
|
||||
|
||||
form_id : str | None
|
||||
The ID of the form that the element belongs to. `None` or empty string
|
||||
if the element doesn't belong to a form or doesn't support forms.
|
||||
|
||||
kwargs : SAFE_VALUES | Iterable[SAFE_VALUES]
|
||||
The arguments to use to compute the element ID.
|
||||
The arguments must be stable, deterministic values.
|
||||
Some common parameters like key, disabled,
|
||||
format_func, label_visibility, args, kwargs, on_change, and
|
||||
the active_script_hash are not supposed to be added here
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
# If form_id is provided, add it to the kwargs.
|
||||
kwargs_to_use = {"form_id": form_id, **kwargs} if form_id else kwargs
|
||||
|
||||
if ctx:
|
||||
# Add the active script hash to give elements on different
|
||||
# pages unique IDs.
|
||||
kwargs_to_use["active_script_hash"] = ctx.active_script_hash
|
||||
|
||||
element_id = _compute_element_id(
|
||||
element_type,
|
||||
user_key,
|
||||
**kwargs_to_use,
|
||||
)
|
||||
|
||||
if ctx:
|
||||
_register_element_id(ctx, element_type, element_id)
|
||||
return element_id
|
||||
|
||||
|
||||
def save_for_app_testing(ctx: ScriptRunContext, k: str, v: Any):
|
||||
if config.get_option("global.appTest"):
|
||||
try:
|
||||
ctx.session_state[TESTING_KEY][k] = v
|
||||
except KeyError:
|
||||
ctx.session_state[TESTING_KEY] = {k: v}
|
||||
508
myenv/lib/python3.11/site-packages/streamlit/elements/map.py
Normal file
508
myenv/lib/python3.11/site-packages/streamlit/elements/map.py
Normal file
@@ -0,0 +1,508 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A wrapper for simple PyDeck scatter charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import streamlit.elements.deck_gl_json_chart as deck_gl_json_chart
|
||||
from streamlit import config, dataframe_util
|
||||
from streamlit.elements.lib.color_util import (
|
||||
Color,
|
||||
IntColorTuple,
|
||||
is_color_like,
|
||||
to_int_color_tuple,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.DeckGlJsonChart_pb2 import DeckGlJsonChart as DeckGlJsonChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Collection
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
from streamlit.dataframe_util import Data
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
# Map used as the basis for st.map.
|
||||
_DEFAULT_MAP: Final[dict[str, Any]] = dict(deck_gl_json_chart.EMPTY_MAP)
|
||||
|
||||
# Other default parameters for st.map.
|
||||
_DEFAULT_LAT_COL_NAMES: Final = {"lat", "latitude", "LAT", "LATITUDE"}
|
||||
_DEFAULT_LON_COL_NAMES: Final = {"lon", "longitude", "LON", "LONGITUDE"}
|
||||
_DEFAULT_COLOR: Final = (200, 30, 0, 160)
|
||||
_DEFAULT_SIZE: Final = 100
|
||||
_DEFAULT_ZOOM_LEVEL: Final = 12
|
||||
_ZOOM_LEVELS: Final = [
|
||||
360,
|
||||
180,
|
||||
90,
|
||||
45,
|
||||
22.5,
|
||||
11.25,
|
||||
5.625,
|
||||
2.813,
|
||||
1.406,
|
||||
0.703,
|
||||
0.352,
|
||||
0.176,
|
||||
0.088,
|
||||
0.044,
|
||||
0.022,
|
||||
0.011,
|
||||
0.005,
|
||||
0.003,
|
||||
0.001,
|
||||
0.0005,
|
||||
0.00025,
|
||||
]
|
||||
|
||||
|
||||
class MapMixin:
|
||||
@gather_metrics("map")
|
||||
def map(
|
||||
self,
|
||||
data: Data = None,
|
||||
*,
|
||||
latitude: str | None = None,
|
||||
longitude: str | None = None,
|
||||
color: None | str | Color = None,
|
||||
size: None | str | float = None,
|
||||
zoom: int | None = None,
|
||||
use_container_width: bool = True,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a map with a scatterplot overlaid onto it.
|
||||
|
||||
This is a wrapper around ``st.pydeck_chart`` to quickly create
|
||||
scatterplot charts on top of a map, with auto-centering and auto-zoom.
|
||||
|
||||
When using this command, Mapbox provides the map tiles to render map
|
||||
content. Note that Mapbox is a third-party product and Streamlit accepts
|
||||
no responsibility or liability of any kind for Mapbox or for any content
|
||||
or information made available by Mapbox.
|
||||
|
||||
Mapbox requires users to register and provide a token before users can
|
||||
request map tiles. Currently, Streamlit provides this token for you, but
|
||||
this could change at any time. We strongly recommend all users create and
|
||||
use their own personal Mapbox token to avoid any disruptions to their
|
||||
experience. You can do this with the ``mapbox.token`` config option. The
|
||||
use of Mapbox is governed by Mapbox's Terms of Use.
|
||||
|
||||
To get a token for yourself, create an account at https://mapbox.com.
|
||||
For more info on how to set config options, see
|
||||
https://docs.streamlit.io/develop/api-reference/configuration/config.toml.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : Anything supported by st.dataframe
|
||||
The data to be plotted.
|
||||
|
||||
latitude : str or None
|
||||
The name of the column containing the latitude coordinates of
|
||||
the datapoints in the chart.
|
||||
|
||||
If None, the latitude data will come from any column named 'lat',
|
||||
'latitude', 'LAT', or 'LATITUDE'.
|
||||
|
||||
longitude : str or None
|
||||
The name of the column containing the longitude coordinates of
|
||||
the datapoints in the chart.
|
||||
|
||||
If None, the longitude data will come from any column named 'lon',
|
||||
'longitude', 'LON', or 'LONGITUDE'.
|
||||
|
||||
color : str or tuple or None
|
||||
The color of the circles representing each datapoint.
|
||||
|
||||
Can be:
|
||||
|
||||
- None, to use the default color.
|
||||
- A hex string like "#ffaa00" or "#ffaa0088".
|
||||
- An RGB or RGBA tuple with the red, green, blue, and alpha
|
||||
components specified as ints from 0 to 255 or floats from 0.0 to
|
||||
1.0.
|
||||
- The name of the column to use for the color. Cells in this column
|
||||
should contain colors represented as a hex string or color tuple,
|
||||
as described above.
|
||||
|
||||
size : str or float or None
|
||||
The size of the circles representing each point, in meters.
|
||||
|
||||
This can be:
|
||||
|
||||
- None, to use the default size.
|
||||
- A number like 100, to specify a single size to use for all
|
||||
datapoints.
|
||||
- The name of the column to use for the size. This allows each
|
||||
datapoint to be represented by a circle of a different size.
|
||||
|
||||
zoom : int
|
||||
Zoom level as specified in
|
||||
https://wiki.openstreetmap.org/wiki/Zoom_levels.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the map's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True``
|
||||
(default), Streamlit sets the width of the map to match the width
|
||||
of the parent container. If ``use_container_width`` is ``False``,
|
||||
Streamlit sets the width of the chart to fit its contents according
|
||||
to the plotting library, up to the width of the parent container.
|
||||
|
||||
width : int or None
|
||||
Desired width of the chart expressed in pixels. If ``width`` is
|
||||
``None`` (default), Streamlit sets the width of the chart to fit
|
||||
its contents according to the plotting library, up to the width of
|
||||
the parent container. If ``width`` is greater than the width of the
|
||||
parent container, Streamlit sets the chart width to match the width
|
||||
of the parent container.
|
||||
|
||||
To use ``width``, you must set ``use_container_width=False``.
|
||||
|
||||
height : int or None
|
||||
Desired height of the chart expressed in pixels. If ``height`` is
|
||||
``None`` (default), Streamlit sets the height of the chart to fit
|
||||
its contents according to the plotting library.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... np.random.randn(1000, 2) / [50, 50] + [37.76, -122.4],
|
||||
... columns=["lat", "lon"],
|
||||
... )
|
||||
>>> st.map(df)
|
||||
|
||||
.. output::
|
||||
https://doc-map.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
You can also customize the size and color of the datapoints:
|
||||
|
||||
>>> st.map(df, size=20, color="#0044ff")
|
||||
|
||||
And finally, you can choose different columns to use for the latitude
|
||||
and longitude components, as well as set size and color of each
|
||||
datapoint dynamically based on other columns:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
... {
|
||||
... "col1": np.random.randn(1000) / 50 + 37.76,
|
||||
... "col2": np.random.randn(1000) / 50 + -122.4,
|
||||
... "col3": np.random.randn(1000) * 100,
|
||||
... "col4": np.random.rand(1000, 4).tolist(),
|
||||
... }
|
||||
... )
|
||||
>>>
|
||||
>>> st.map(df, latitude="col1", longitude="col2", size="col3", color="col4")
|
||||
|
||||
.. output::
|
||||
https://doc-map-color.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
# This feature was turned off while we investigate why different
|
||||
# map styles cause DeckGL to crash.
|
||||
#
|
||||
# For reference, this was the docstring for map_style:
|
||||
#
|
||||
# map_style : str or None
|
||||
# One of Mapbox's map style URLs. A full list can be found here:
|
||||
# https://docs.mapbox.com/api/maps/styles/#mapbox-styles
|
||||
#
|
||||
# This feature requires a Mapbox token. See the top of these docs
|
||||
# for information on how to get one and set it up in Streamlit.
|
||||
#
|
||||
map_style = None
|
||||
map_proto = DeckGlJsonChartProto()
|
||||
deck_gl_json = to_deckgl_json(
|
||||
data, latitude, longitude, size, color, map_style, zoom
|
||||
)
|
||||
marshall(
|
||||
map_proto, deck_gl_json, use_container_width, width=width, height=height
|
||||
)
|
||||
return self.dg._enqueue("deck_gl_json_chart", map_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def to_deckgl_json(
|
||||
data: Data,
|
||||
lat: str | None,
|
||||
lon: str | None,
|
||||
size: None | str | float,
|
||||
color: None | str | Collection[float],
|
||||
map_style: str | None,
|
||||
zoom: int | None,
|
||||
) -> str:
|
||||
if data is None:
|
||||
return json.dumps(_DEFAULT_MAP)
|
||||
|
||||
# TODO(harahu): iterables don't have the empty attribute. This is either
|
||||
# a bug, or the documented data type is too broad. One or the other
|
||||
# should be addressed
|
||||
if hasattr(data, "empty") and data.empty:
|
||||
return json.dumps(_DEFAULT_MAP)
|
||||
|
||||
df = dataframe_util.convert_anything_to_pandas_df(data)
|
||||
|
||||
lat_col_name = _get_lat_or_lon_col_name(df, "latitude", lat, _DEFAULT_LAT_COL_NAMES)
|
||||
lon_col_name = _get_lat_or_lon_col_name(
|
||||
df, "longitude", lon, _DEFAULT_LON_COL_NAMES
|
||||
)
|
||||
size_arg, size_col_name = _get_value_and_col_name(df, size, _DEFAULT_SIZE)
|
||||
color_arg, color_col_name = _get_value_and_col_name(df, color, _DEFAULT_COLOR)
|
||||
|
||||
# Drop columns we're not using.
|
||||
# (Sort for tests)
|
||||
used_columns = sorted(
|
||||
[
|
||||
c
|
||||
for c in {lat_col_name, lon_col_name, size_col_name, color_col_name}
|
||||
if c is not None
|
||||
]
|
||||
)
|
||||
df = df[used_columns]
|
||||
|
||||
color_arg = _convert_color_arg_or_column(df, color_arg, color_col_name)
|
||||
|
||||
zoom, center_lat, center_lon = _get_viewport_details(
|
||||
df, lat_col_name, lon_col_name, zoom
|
||||
)
|
||||
|
||||
default = copy.deepcopy(_DEFAULT_MAP)
|
||||
default["initialViewState"]["latitude"] = center_lat
|
||||
default["initialViewState"]["longitude"] = center_lon
|
||||
default["initialViewState"]["zoom"] = zoom
|
||||
default["layers"] = [
|
||||
{
|
||||
"@@type": "ScatterplotLayer",
|
||||
"getPosition": f"@@=[{lon_col_name}, {lat_col_name}]",
|
||||
"getRadius": size_arg,
|
||||
"radiusMinPixels": 3,
|
||||
"radiusUnits": "meters",
|
||||
"getFillColor": color_arg,
|
||||
"data": df.to_dict("records"),
|
||||
}
|
||||
]
|
||||
|
||||
if map_style:
|
||||
if not config.get_option("mapbox.token"):
|
||||
raise StreamlitAPIException(
|
||||
"You need a Mapbox token in order to select a map type. "
|
||||
"Refer to the docs for st.map for more information."
|
||||
)
|
||||
default["mapStyle"] = map_style
|
||||
|
||||
return json.dumps(default)
|
||||
|
||||
|
||||
def _get_lat_or_lon_col_name(
|
||||
data: DataFrame,
|
||||
human_readable_name: str,
|
||||
col_name_from_user: str | None,
|
||||
default_col_names: set[str],
|
||||
) -> str:
|
||||
"""Returns the column name to be used for latitude or longitude."""
|
||||
|
||||
if isinstance(col_name_from_user, str) and col_name_from_user in data.columns:
|
||||
col_name = col_name_from_user
|
||||
|
||||
else:
|
||||
# Try one of the default col_names:
|
||||
candidate_col_name = None
|
||||
|
||||
for c in default_col_names:
|
||||
if c in data.columns:
|
||||
candidate_col_name = c
|
||||
break
|
||||
|
||||
if candidate_col_name is None:
|
||||
formatted_allowed_col_name = ", ".join(map(repr, sorted(default_col_names)))
|
||||
formmated_col_names = ", ".join(map(repr, list(data.columns)))
|
||||
|
||||
raise StreamlitAPIException(
|
||||
f"Map data must contain a {human_readable_name} column named: "
|
||||
f"{formatted_allowed_col_name}. Existing columns: {formmated_col_names}"
|
||||
)
|
||||
else:
|
||||
col_name = candidate_col_name
|
||||
|
||||
# Check that the column is well-formed.
|
||||
# IMPLEMENTATION NOTE: We can't use isnull().values.any() because .values can return
|
||||
# ExtensionArrays, which don't have a .any() method.
|
||||
# (Read about ExtensionArrays here: # https://pandas.pydata.org/community/blog/extension-arrays.html)
|
||||
# However, after a performance test I found the solution below runs basically as
|
||||
# fast as .values.any().
|
||||
if any(data[col_name].isna().array):
|
||||
raise StreamlitAPIException(
|
||||
f"Column {col_name} is not allowed to contain null values, such "
|
||||
"as NaN, NaT, or None."
|
||||
)
|
||||
|
||||
return col_name
|
||||
|
||||
|
||||
def _get_value_and_col_name(
|
||||
data: DataFrame,
|
||||
value_or_name: Any,
|
||||
default_value: Any,
|
||||
) -> tuple[Any, str | None]:
|
||||
"""Take a value_or_name passed in by the Streamlit developer and return a PyDeck
|
||||
argument and column name for that property.
|
||||
|
||||
This is used for the size and color properties of the chart.
|
||||
|
||||
Example:
|
||||
- If the user passes size=None, this returns the default size value and no column.
|
||||
- If the user passes size=42, this returns 42 and no column.
|
||||
- If the user passes size="my_col_123", this returns "@@=my_col_123" and "my_col_123".
|
||||
"""
|
||||
|
||||
pydeck_arg: str | float
|
||||
|
||||
if isinstance(value_or_name, str) and value_or_name in data.columns:
|
||||
col_name = value_or_name
|
||||
pydeck_arg = f"@@={col_name}"
|
||||
else:
|
||||
col_name = None
|
||||
|
||||
if value_or_name is None:
|
||||
pydeck_arg = default_value
|
||||
else:
|
||||
pydeck_arg = value_or_name
|
||||
|
||||
return pydeck_arg, col_name
|
||||
|
||||
|
||||
def _convert_color_arg_or_column(
|
||||
data: DataFrame,
|
||||
color_arg: str | Color,
|
||||
color_col_name: str | None,
|
||||
) -> None | str | IntColorTuple:
|
||||
"""Converts color to a format accepted by PyDeck.
|
||||
|
||||
For example:
|
||||
- If color_arg is "#fff", then returns (255, 255, 255, 255).
|
||||
- If color_col_name is "my_col_123", then it converts everything in column my_col_123 to
|
||||
an accepted color format such as (0, 100, 200, 255).
|
||||
|
||||
NOTE: This function mutates the data argument.
|
||||
"""
|
||||
|
||||
color_arg_out: None | str | IntColorTuple = None
|
||||
|
||||
if color_col_name is not None:
|
||||
# Convert color column to the right format.
|
||||
if len(data[color_col_name]) > 0 and is_color_like(
|
||||
data[color_col_name].iloc[0]
|
||||
):
|
||||
# Use .loc[] to avoid a SettingWithCopyWarning in some cases.
|
||||
data.loc[:, color_col_name] = data.loc[:, color_col_name].map(
|
||||
to_int_color_tuple
|
||||
)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f'Column "{color_col_name}" does not appear to contain valid colors.'
|
||||
)
|
||||
|
||||
# This is guaranteed to be a str because of _get_value_and_col_name
|
||||
assert isinstance(color_arg, str)
|
||||
color_arg_out = color_arg
|
||||
|
||||
elif color_arg is not None:
|
||||
color_arg_out = to_int_color_tuple(color_arg)
|
||||
|
||||
return color_arg_out
|
||||
|
||||
|
||||
def _get_viewport_details(
|
||||
data: DataFrame, lat_col_name: str, lon_col_name: str, zoom: int | None
|
||||
) -> tuple[int, float, float]:
|
||||
"""Auto-set viewport when not fully specified by user."""
|
||||
min_lat = data[lat_col_name].min()
|
||||
max_lat = data[lat_col_name].max()
|
||||
min_lon = data[lon_col_name].min()
|
||||
max_lon = data[lon_col_name].max()
|
||||
center_lat = (max_lat + min_lat) / 2.0
|
||||
center_lon = (max_lon + min_lon) / 2.0
|
||||
range_lon = abs(max_lon - min_lon)
|
||||
range_lat = abs(max_lat - min_lat)
|
||||
|
||||
if zoom is None:
|
||||
if range_lon > range_lat:
|
||||
longitude_distance = range_lon
|
||||
else:
|
||||
longitude_distance = range_lat
|
||||
zoom = _get_zoom_level(longitude_distance)
|
||||
|
||||
return zoom, center_lat, center_lon
|
||||
|
||||
|
||||
def _get_zoom_level(distance: float) -> int:
|
||||
"""Get the zoom level for a given distance in degrees.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/Zoom_levels for reference.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
distance : float
|
||||
How many degrees of longitude should fit in the map.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The zoom level, from 0 to 20.
|
||||
|
||||
"""
|
||||
for i in range(len(_ZOOM_LEVELS) - 1):
|
||||
if _ZOOM_LEVELS[i + 1] < distance <= _ZOOM_LEVELS[i]:
|
||||
return i
|
||||
|
||||
# For small number of points the default zoom level will be used.
|
||||
return _DEFAULT_ZOOM_LEVEL
|
||||
|
||||
|
||||
def marshall(
|
||||
pydeck_proto: DeckGlJsonChartProto,
|
||||
pydeck_json: str,
|
||||
use_container_width: bool,
|
||||
height: int | None = None,
|
||||
width: int | None = None,
|
||||
) -> None:
|
||||
pydeck_proto.json = pydeck_json
|
||||
pydeck_proto.use_container_width = use_container_width
|
||||
|
||||
if width:
|
||||
pydeck_proto.width = width
|
||||
if height:
|
||||
pydeck_proto.height = height
|
||||
|
||||
pydeck_proto.id = ""
|
||||
@@ -0,0 +1,388 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Final, Literal, cast
|
||||
|
||||
from streamlit.proto.Markdown_pb2 import Markdown as MarkdownProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text, validate_icon_or_emoji
|
||||
from streamlit.type_util import SupportsStr, is_sympy_expression
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import sympy
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
MARKDOWN_HORIZONTAL_RULE_EXPRESSION: Final = "---"
|
||||
|
||||
|
||||
class MarkdownMixin:
|
||||
@gather_metrics("markdown")
|
||||
def markdown(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
unsafe_allow_html: bool = False,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
r"""Display string formatted as Markdown.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : any
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
If anything other than a string is passed, it will be converted
|
||||
into a string behind the scenes using ``str(body)``.
|
||||
|
||||
This also supports:
|
||||
|
||||
- Emoji shortcodes, such as ``:+1:`` and ``:sunglasses:``.
|
||||
For a list of all supported codes,
|
||||
see https://share.streamlit.io/streamlit/emoji-shortcodes.
|
||||
|
||||
- Streamlit logo shortcode. Use ``:streamlit:`` to add a little
|
||||
Streamlit flair to your text.
|
||||
|
||||
- A limited set of typographical symbols. ``"<- -> <-> -- >= <= ~="``
|
||||
becomes "← → ↔ — ≥ ≤ ≈" when parsed as Markdown.
|
||||
|
||||
- Google Material Symbols (rounded style), using the syntax
|
||||
``:material/icon_name:``, where "icon_name" is the name of the
|
||||
icon in snake case. For a complete list of icons, see Google's
|
||||
`Material Symbols <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
- LaTeX expressions, by wrapping them in "$" or "$$" (the "$$"
|
||||
must be on their own lines). Supported LaTeX functions are listed
|
||||
at https://katex.org/docs/supported.html.
|
||||
|
||||
- Colored text and background colors for text, using the syntax
|
||||
``:color[text to be colored]`` and ``:color-background[text to be colored]``,
|
||||
respectively. ``color`` must be replaced with any of the following
|
||||
supported colors: blue, green, orange, red, violet, gray/grey,
|
||||
rainbow, or primary. For example, you can use
|
||||
``:orange[your text here]`` or ``:blue-background[your text here]``.
|
||||
If you use "primary" for color, Streamlit will use the default
|
||||
primary accent color unless you set the ``theme.primaryColor``
|
||||
configuration option.
|
||||
|
||||
- Colored badges, using the syntax ``:color-badge[text in the badge]``.
|
||||
``color`` must be replaced with any of the following supported
|
||||
colors: blue, green, orange, red, violet, gray/grey, or primary.
|
||||
For example, you can use ``:orange-badge[your text here]`` or
|
||||
``:blue-badge[your text here]``.
|
||||
|
||||
- Small text, using the syntax ``:small[text to show small]``.
|
||||
|
||||
|
||||
unsafe_allow_html : bool
|
||||
Whether to render HTML within ``body``. If this is ``False``
|
||||
(default), any HTML tags found in ``body`` will be escaped and
|
||||
therefore treated as raw text. If this is ``True``, any HTML
|
||||
expressions within ``body`` will be rendered.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability.
|
||||
|
||||
.. note::
|
||||
If you only want to insert HTML or CSS without Markdown text,
|
||||
we recommend using ``st.html`` instead.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the Markdown. If this is
|
||||
``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.markdown("*Streamlit* is **really** ***cool***.")
|
||||
>>> st.markdown('''
|
||||
... :red[Streamlit] :orange[can] :green[write] :blue[text] :violet[in]
|
||||
... :gray[pretty] :rainbow[colors] and :blue-background[highlight] text.''')
|
||||
>>> st.markdown("Here's a bouquet —\
|
||||
... :tulip::cherry_blossom::rose::hibiscus::sunflower::blossom:")
|
||||
>>>
|
||||
>>> multi = '''If you end a line with two spaces,
|
||||
... a soft return is used for the next line.
|
||||
...
|
||||
... Two (or more) newline characters in a row will result in a hard return.
|
||||
... '''
|
||||
>>> st.markdown(multi)
|
||||
|
||||
.. output::
|
||||
https://doc-markdown.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
markdown_proto = MarkdownProto()
|
||||
|
||||
markdown_proto.body = clean_text(body)
|
||||
markdown_proto.allow_html = unsafe_allow_html
|
||||
markdown_proto.element_type = MarkdownProto.Type.NATIVE
|
||||
if help:
|
||||
markdown_proto.help = help
|
||||
|
||||
return self.dg._enqueue("markdown", markdown_proto)
|
||||
|
||||
@gather_metrics("caption")
|
||||
def caption(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
unsafe_allow_html: bool = False,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display text in small font.
|
||||
|
||||
This should be used for captions, asides, footnotes, sidenotes, and
|
||||
other explanatory text.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The text to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
unsafe_allow_html : bool
|
||||
Whether to render HTML within ``body``. If this is ``False``
|
||||
(default), any HTML tags found in ``body`` will be escaped and
|
||||
therefore treated as raw text. If this is ``True``, any HTML
|
||||
expressions within ``body`` will be rendered.
|
||||
|
||||
Adding custom HTML to your app impacts safety, styling, and
|
||||
maintainability.
|
||||
|
||||
.. note::
|
||||
If you only want to insert HTML or CSS without Markdown text,
|
||||
we recommend using ``st.html`` instead.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the caption. If this is
|
||||
``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.caption("This is a string that explains something above.")
|
||||
>>> st.caption("A caption with _italics_ :blue[colors] and emojis :sunglasses:")
|
||||
|
||||
"""
|
||||
caption_proto = MarkdownProto()
|
||||
caption_proto.body = clean_text(body)
|
||||
caption_proto.allow_html = unsafe_allow_html
|
||||
caption_proto.is_caption = True
|
||||
caption_proto.element_type = MarkdownProto.Type.CAPTION
|
||||
if help:
|
||||
caption_proto.help = help
|
||||
return self.dg._enqueue("markdown", caption_proto)
|
||||
|
||||
@gather_metrics("latex")
|
||||
def latex(
|
||||
self,
|
||||
body: SupportsStr | sympy.Expr,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
# This docstring needs to be "raw" because of the backslashes in the
|
||||
# example below.
|
||||
r"""Display mathematical expressions formatted as LaTeX.
|
||||
|
||||
Supported LaTeX functions are listed at
|
||||
https://katex.org/docs/supported.html.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str or SymPy expression
|
||||
The string or SymPy expression to display as LaTeX. If str, it's
|
||||
a good idea to use raw Python strings since LaTeX uses backslashes
|
||||
a lot.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the LaTeX expression. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.latex(r'''
|
||||
... a + ar + a r^2 + a r^3 + \cdots + a r^{n-1} =
|
||||
... \sum_{k=0}^{n-1} ar^k =
|
||||
... a \left(\frac{1-r^{n}}{1-r}\right)
|
||||
... ''')
|
||||
|
||||
"""
|
||||
if is_sympy_expression(body):
|
||||
import sympy
|
||||
|
||||
body = sympy.latex(body)
|
||||
|
||||
latex_proto = MarkdownProto()
|
||||
latex_proto.body = "$$\n%s\n$$" % clean_text(body)
|
||||
latex_proto.element_type = MarkdownProto.Type.LATEX
|
||||
if help:
|
||||
latex_proto.help = help
|
||||
return self.dg._enqueue("markdown", latex_proto)
|
||||
|
||||
@gather_metrics("divider")
|
||||
def divider(self) -> DeltaGenerator:
|
||||
"""Display a horizontal rule.
|
||||
|
||||
.. note::
|
||||
You can achieve the same effect with st.write("---") or
|
||||
even just "---" in your script (via magic).
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.divider()
|
||||
|
||||
"""
|
||||
divider_proto = MarkdownProto()
|
||||
divider_proto.body = MARKDOWN_HORIZONTAL_RULE_EXPRESSION
|
||||
divider_proto.element_type = MarkdownProto.Type.DIVIDER
|
||||
return self.dg._enqueue("markdown", divider_proto)
|
||||
|
||||
@gather_metrics("badge")
|
||||
def badge(
|
||||
self,
|
||||
label: str,
|
||||
*, # keyword-only arguments:
|
||||
icon: str | None = None,
|
||||
color: Literal[
|
||||
"blue",
|
||||
"green",
|
||||
"orange",
|
||||
"red",
|
||||
"violet",
|
||||
"gray",
|
||||
"grey",
|
||||
"rainbow",
|
||||
"primary",
|
||||
] = "blue",
|
||||
) -> DeltaGenerator:
|
||||
"""Display a colored badge with an icon and label.
|
||||
|
||||
This is a thin wrapper around the color-badge Markdown directive.
|
||||
The following are equivalent:
|
||||
|
||||
- ``st.markdown(":blue-badge[Home]")``
|
||||
- ``st.badge("Home", color="blue")``
|
||||
|
||||
.. note::
|
||||
You can insert badges everywhere Streamlit supports Markdown by
|
||||
using the color-badge Markdown directive. See ``st.markdown`` for
|
||||
more information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The label to display in the badge. The label can optionally contain
|
||||
GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives. Because this command escapes square
|
||||
brackets (``[ ]``) in this parameter, any directive requiring
|
||||
square brackets is not supported.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
icon : str or None
|
||||
An optional emoji or icon to display next to the badge label. If
|
||||
``icon`` is ``None`` (default), no icon is displayed. If ``icon``
|
||||
is a string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
color : str
|
||||
The color to use for the badge. This defaults to ``"blue"``.
|
||||
|
||||
This can be one of the following supported colors: blue, green,
|
||||
orange, red, violet, gray/grey, or primary. If you use
|
||||
``"primary"``, Streamlit will use the default primary accent color
|
||||
unless you set the ``theme.primaryColor`` configuration option.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Create standalone badges with ``st.badge`` (with or without icons). If
|
||||
you want to have multiple, side-by-side badges, you can use the
|
||||
Markdown directive in ``st.markdown``.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.badge("New")
|
||||
>>> st.badge("Success", icon=":material/check:", color="green")
|
||||
>>>
|
||||
>>> st.markdown(
|
||||
>>> ":violet-badge[:material/star: Favorite] :orange-badge[⚠️ Needs review] :gray-badge[Deprecated]"
|
||||
>>> )
|
||||
|
||||
.. output ::
|
||||
https://doc-badge.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
if icon is not None:
|
||||
icon_str = validate_icon_or_emoji(icon) + " "
|
||||
else:
|
||||
icon_str = ""
|
||||
|
||||
# Escape [ and ] characters in the label to prevent breaking the directive syntax
|
||||
escaped_label = label.replace("[", "\\[").replace("]", "\\]")
|
||||
|
||||
badge_proto = MarkdownProto()
|
||||
badge_proto.body = f":{color}-badge[{icon_str}{escaped_label}]"
|
||||
badge_proto.element_type = MarkdownProto.Type.NATIVE
|
||||
return self.dg._enqueue("markdown", badge_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
794
myenv/lib/python3.11/site-packages/streamlit/elements/media.py
Normal file
794
myenv/lib/python3.11/site-packages/streamlit/elements/media.py
Normal file
@@ -0,0 +1,794 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import re
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Final, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import runtime, type_util, url_util
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.subtitle_utils import process_subtitle_data
|
||||
from streamlit.elements.lib.utils import compute_and_register_element_id
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Audio_pb2 import Audio as AudioProto
|
||||
from streamlit.proto.Video_pb2 import Video as VideoProto
|
||||
from streamlit.runtime import caching
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.time_util import time_to_seconds
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from numpy import typing as npt
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import NumpyShape
|
||||
|
||||
|
||||
MediaData: TypeAlias = Union[
|
||||
str,
|
||||
Path,
|
||||
bytes,
|
||||
io.BytesIO,
|
||||
io.RawIOBase,
|
||||
io.BufferedReader,
|
||||
"npt.NDArray[Any]",
|
||||
None,
|
||||
]
|
||||
|
||||
SubtitleData: TypeAlias = Union[
|
||||
str, Path, bytes, io.BytesIO, dict[str, Union[str, Path, bytes, io.BytesIO]], None
|
||||
]
|
||||
|
||||
MediaTime: TypeAlias = Union[int, float, timedelta, str]
|
||||
|
||||
TIMEDELTA_PARSE_ERROR_MESSAGE: Final = (
|
||||
"Failed to convert '{param_name}' to a timedelta. "
|
||||
"Please use a string in a format supported by "
|
||||
"[Pandas Timedelta constructor]"
|
||||
"(https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html), "
|
||||
'e.g. `"10s"`, `"15 seconds"`, or `"1h23s"`. Got: {param_value}'
|
||||
)
|
||||
|
||||
|
||||
class MediaMixin:
|
||||
@gather_metrics("audio")
|
||||
def audio(
|
||||
self,
|
||||
data: MediaData,
|
||||
format: str = "audio/wav",
|
||||
start_time: MediaTime = 0,
|
||||
*,
|
||||
sample_rate: int | None = None,
|
||||
end_time: MediaTime | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display an audio player.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : str, Path, bytes, BytesIO, numpy.ndarray, or file
|
||||
The audio to play. This can be one of the following:
|
||||
|
||||
- A URL (string) for a hosted audio file.
|
||||
- A path to a local audio file. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- Raw audio data. Raw data formats must include all necessary file
|
||||
headers to match the file format specified via ``format``.
|
||||
|
||||
If ``data`` is a NumPy array, it must either be a 1D array of the
|
||||
waveform or a 2D array of shape (C, S) where C is the number of
|
||||
channels and S is the number of samples. See the default channel
|
||||
order at
|
||||
http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
|
||||
|
||||
format : str
|
||||
The MIME type for the audio file. This defaults to ``"audio/wav"``.
|
||||
For more information about MIME types, see
|
||||
https://www.iana.org/assignments/media-types/media-types.xhtml.
|
||||
|
||||
start_time: int, float, timedelta, str, or None
|
||||
The time from which the element should start playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays from the beginning.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
sample_rate: int or None
|
||||
The sample rate of the audio data in samples per second. This is
|
||||
only required if ``data`` is a NumPy array.
|
||||
end_time: int, float, timedelta, str, or None
|
||||
The time at which the element should stop playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays through to the end.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
loop: bool
|
||||
Whether the audio should loop playback.
|
||||
autoplay: bool
|
||||
Whether the audio file should start playing automatically. This is
|
||||
``False`` by default. Browsers will not autoplay audio files if the
|
||||
user has not interacted with the page by clicking somewhere.
|
||||
|
||||
Examples
|
||||
--------
|
||||
To display an audio player for a local file, specify the file's string
|
||||
path and format.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.audio("cat-purr.mp3", format="audio/mpeg", loop=True)
|
||||
|
||||
.. output::
|
||||
https://doc-audio-purr.streamlit.app/
|
||||
height: 250px
|
||||
|
||||
You can also pass ``bytes`` or ``numpy.ndarray`` objects to ``st.audio``.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> audio_file = open("myaudio.ogg", "rb")
|
||||
>>> audio_bytes = audio_file.read()
|
||||
>>>
|
||||
>>> st.audio(audio_bytes, format="audio/ogg")
|
||||
>>>
|
||||
>>> sample_rate = 44100 # 44100 samples per second
|
||||
>>> seconds = 2 # Note duration of 2 seconds
|
||||
>>> frequency_la = 440 # Our played note will be 440 Hz
|
||||
>>> # Generate array with seconds*sample_rate steps, ranging between 0 and seconds
|
||||
>>> t = np.linspace(0, seconds, seconds * sample_rate, False)
|
||||
>>> # Generate a 440 Hz sine wave
|
||||
>>> note_la = np.sin(frequency_la * t * 2 * np.pi)
|
||||
>>>
|
||||
>>> st.audio(note_la, sample_rate=sample_rate)
|
||||
|
||||
.. output::
|
||||
https://doc-audio.streamlit.app/
|
||||
height: 865px
|
||||
|
||||
"""
|
||||
start_time, end_time = _parse_start_time_end_time(start_time, end_time)
|
||||
|
||||
audio_proto = AudioProto()
|
||||
|
||||
is_data_numpy_array = type_util.is_type(data, "numpy.ndarray")
|
||||
|
||||
if is_data_numpy_array and sample_rate is None:
|
||||
raise StreamlitAPIException(
|
||||
"`sample_rate` must be specified when `data` is a numpy array."
|
||||
)
|
||||
if not is_data_numpy_array and sample_rate is not None:
|
||||
self.dg.warning(
|
||||
"Warning: `sample_rate` will be ignored since data is not a numpy "
|
||||
"array."
|
||||
)
|
||||
coordinates = self.dg._get_delta_path_str()
|
||||
marshall_audio(
|
||||
coordinates,
|
||||
audio_proto,
|
||||
data,
|
||||
format,
|
||||
start_time,
|
||||
sample_rate,
|
||||
end_time,
|
||||
loop,
|
||||
autoplay,
|
||||
form_id=current_form_id(self.dg),
|
||||
)
|
||||
return self.dg._enqueue("audio", audio_proto)
|
||||
|
||||
@gather_metrics("video")
|
||||
def video(
|
||||
self,
|
||||
data: MediaData,
|
||||
format: str = "video/mp4",
|
||||
start_time: MediaTime = 0,
|
||||
*, # keyword-only arguments:
|
||||
subtitles: SubtitleData = None,
|
||||
end_time: MediaTime | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
muted: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a video player.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : str, Path, bytes, io.BytesIO, numpy.ndarray, or file
|
||||
The video to play. This can be one of the following:
|
||||
|
||||
- A URL (string) for a hosted video file, including YouTube URLs.
|
||||
- A path to a local video file. The path can be a ``str``
|
||||
or ``Path`` object. Paths can be absolute or relative to the
|
||||
working directory (where you execute ``streamlit run``).
|
||||
- Raw video data. Raw data formats must include all necessary file
|
||||
headers to match the file format specified via ``format``.
|
||||
|
||||
format : str
|
||||
The MIME type for the video file. This defaults to ``"video/mp4"``.
|
||||
For more information about MIME types, see
|
||||
https://www.iana.org/assignments/media-types/media-types.xhtml.
|
||||
|
||||
start_time: int, float, timedelta, str, or None
|
||||
The time from which the element should start playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays from the beginning.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
subtitles: str, bytes, Path, io.BytesIO, or dict
|
||||
Optional subtitle data for the video, supporting several input types:
|
||||
|
||||
- ``None`` (default): No subtitles.
|
||||
|
||||
- A string, bytes, or Path: File path to a subtitle file in
|
||||
``.vtt`` or ``.srt`` formats, or the raw content of subtitles
|
||||
conforming to these formats. Paths can be absolute or relative to
|
||||
the working directory (where you execute ``streamlit run``).
|
||||
If providing raw content, the string must adhere to the WebVTT or
|
||||
SRT format specifications.
|
||||
|
||||
- io.BytesIO: A BytesIO stream that contains valid ``.vtt`` or ``.srt``
|
||||
formatted subtitle data.
|
||||
|
||||
- A dictionary: Pairs of labels and file paths or raw subtitle content in
|
||||
``.vtt`` or ``.srt`` formats to enable multiple subtitle tracks.
|
||||
The label will be shown in the video player. Example:
|
||||
``{"English": "path/to/english.vtt", "French": "path/to/french.srt"}``
|
||||
|
||||
When provided, subtitles are displayed by default. For multiple
|
||||
tracks, the first one is displayed by default. If you don't want any
|
||||
subtitles displayed by default, use an empty string for the value
|
||||
in a dictrionary's first pair: ``{"None": "", "English": "path/to/english.vtt"}``
|
||||
|
||||
Not supported for YouTube videos.
|
||||
end_time: int, float, timedelta, str, or None
|
||||
The time at which the element should stop playing. This can be
|
||||
one of the following:
|
||||
|
||||
- ``None`` (default): The element plays through to the end.
|
||||
- An ``int`` or ``float`` specifying the time in seconds. ``float``
|
||||
values are rounded down to whole seconds.
|
||||
- A string specifying the time in a format supported by `Pandas'
|
||||
Timedelta constructor <https://pandas.pydata.org/docs/reference/api/pandas.Timedelta.html>`_,
|
||||
e.g. ``"2 minute"``, ``"20s"``, or ``"1m14s"``.
|
||||
- A ``timedelta`` object from `Python's built-in datetime library
|
||||
<https://docs.python.org/3/library/datetime.html#timedelta-objects>`_,
|
||||
e.g. ``timedelta(seconds=70)``.
|
||||
loop: bool
|
||||
Whether the video should loop playback.
|
||||
autoplay: bool
|
||||
Whether the video should start playing automatically. This is
|
||||
``False`` by default. Browsers will not autoplay unmuted videos
|
||||
if the user has not interacted with the page by clicking somewhere.
|
||||
To enable autoplay without user interaction, you must also set
|
||||
``muted=True``.
|
||||
muted: bool
|
||||
Whether the video should play with the audio silenced. This is
|
||||
``False`` by default. Use this in conjunction with ``autoplay=True``
|
||||
to enable autoplay without user interaction.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> video_file = open("myvideo.mp4", "rb")
|
||||
>>> video_bytes = video_file.read()
|
||||
>>>
|
||||
>>> st.video(video_bytes)
|
||||
|
||||
.. output::
|
||||
https://doc-video.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
When you include subtitles, they will be turned on by default. A viewer
|
||||
can turn off the subtitles (or captions) from the browser's default video
|
||||
control menu, usually located in the lower-right corner of the video.
|
||||
|
||||
Here is a simple VTT file (``subtitles.vtt``):
|
||||
|
||||
>>> WEBVTT
|
||||
>>>
|
||||
>>> 0:00:01.000 --> 0:00:02.000
|
||||
>>> Look!
|
||||
>>>
|
||||
>>> 0:00:03.000 --> 0:00:05.000
|
||||
>>> Look at the pretty stars!
|
||||
|
||||
If the above VTT file lives in the same directory as your app, you can
|
||||
add subtitles like so:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> VIDEO_URL = "https://example.com/not-youtube.mp4"
|
||||
>>> st.video(VIDEO_URL, subtitles="subtitles.vtt")
|
||||
|
||||
.. output::
|
||||
https://doc-video-subtitles.streamlit.app/
|
||||
height: 700px
|
||||
|
||||
See additional examples of supported subtitle input types in our
|
||||
`video subtitles feature demo <https://doc-video-subtitle-inputs.streamlit.app/>`_.
|
||||
|
||||
.. note::
|
||||
Some videos may not display if they are encoded using MP4V (which is an export option in OpenCV), as this codec is
|
||||
not widely supported by browsers. Converting your video to H.264 will allow the video to be displayed in Streamlit.
|
||||
See this `StackOverflow post <https://stackoverflow.com/a/49535220/2394542>`_ or this
|
||||
`Streamlit forum post <https://discuss.streamlit.io/t/st-video-doesnt-show-opencv-generated-mp4/3193/2>`_
|
||||
for more information.
|
||||
|
||||
"""
|
||||
start_time, end_time = _parse_start_time_end_time(start_time, end_time)
|
||||
|
||||
video_proto = VideoProto()
|
||||
coordinates = self.dg._get_delta_path_str()
|
||||
marshall_video(
|
||||
coordinates,
|
||||
video_proto,
|
||||
data,
|
||||
format,
|
||||
start_time,
|
||||
subtitles,
|
||||
end_time,
|
||||
loop,
|
||||
autoplay,
|
||||
muted,
|
||||
form_id=current_form_id(self.dg),
|
||||
)
|
||||
return self.dg._enqueue("video", video_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
# Regular expression from
|
||||
# https://gist.github.com/rodrigoborgesdeoliveira/987683cfbfcc8d800192da1e73adc486?permalink_comment_id=4645864#gistcomment-4645864
|
||||
# Covers any youtube URL (incl. shortlinks and embed links) and extracts its video code.
|
||||
YOUTUBE_RE: Final = r"^((https?://(?:www\.)?(?:m\.)?youtube\.com))/((?:oembed\?url=https?%3A//(?:www\.)youtube.com/watch\?(?:v%3D)(?P<video_id_1>[\w\-]{10,20})&format=json)|(?:attribution_link\?a=.*watch(?:%3Fv%3D|%3Fv%3D)(?P<video_id_2>[\w\-]{10,20}))(?:%26feature.*))|(https?:)?(\/\/)?((www\.|m\.)?youtube(-nocookie)?\.com\/((watch)?\?(app=desktop&)?(feature=\w*&)?v=|embed\/|v\/|e\/)|youtu\.be\/)(?P<video_id_3>[\w\-]{10,20})"
|
||||
|
||||
|
||||
def _reshape_youtube_url(url: str) -> str | None:
|
||||
"""Return whether URL is any kind of YouTube embed or watch link. If so,
|
||||
reshape URL into an embed link suitable for use in an iframe.
|
||||
|
||||
If not a YouTube URL, return None.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
url : str
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> print(_reshape_youtube_url("https://youtu.be/_T8LGqJtuGc"))
|
||||
|
||||
.. output::
|
||||
https://www.youtube.com/embed/_T8LGqJtuGc
|
||||
"""
|
||||
match = re.match(YOUTUBE_RE, url)
|
||||
if match:
|
||||
code = (
|
||||
match.group("video_id_1")
|
||||
or match.group("video_id_2")
|
||||
or match.group("video_id_3")
|
||||
)
|
||||
return f"https://www.youtube.com/embed/{code}"
|
||||
return None
|
||||
|
||||
|
||||
def _marshall_av_media(
|
||||
coordinates: str,
|
||||
proto: AudioProto | VideoProto,
|
||||
data: MediaData,
|
||||
mimetype: str,
|
||||
) -> None:
|
||||
"""Fill audio or video proto based on contents of data.
|
||||
|
||||
Given a string, check if it's a url; if so, send it out without modification.
|
||||
Otherwise assume strings are filenames and let any OS errors raise.
|
||||
|
||||
Load data either from file or through bytes-processing methods into a
|
||||
MediaFile object. Pack proto with generated Tornado-based URL.
|
||||
|
||||
(When running in "raw" mode, we won't actually load data into the
|
||||
MediaFileManager, and we'll return an empty URL.)
|
||||
"""
|
||||
# Audio and Video methods have already checked if this is a URL by this point.
|
||||
|
||||
if data is None:
|
||||
# Allow empty values so media players can be shown without media.
|
||||
return
|
||||
|
||||
data_or_filename: bytes | str
|
||||
if isinstance(data, (str, bytes)):
|
||||
# Pass strings and bytes through unchanged
|
||||
data_or_filename = data
|
||||
elif isinstance(data, Path):
|
||||
data_or_filename = str(data)
|
||||
elif isinstance(data, io.BytesIO):
|
||||
data.seek(0)
|
||||
data_or_filename = data.getvalue()
|
||||
elif isinstance(data, io.RawIOBase) or isinstance(data, io.BufferedReader):
|
||||
data.seek(0)
|
||||
read_data = data.read()
|
||||
if read_data is None:
|
||||
return
|
||||
else:
|
||||
data_or_filename = read_data
|
||||
elif type_util.is_type(data, "numpy.ndarray"):
|
||||
data_or_filename = data.tobytes()
|
||||
else:
|
||||
raise RuntimeError("Invalid binary data format: %s" % type(data))
|
||||
|
||||
if runtime.exists():
|
||||
file_url = runtime.get_instance().media_file_mgr.add(
|
||||
data_or_filename, mimetype, coordinates
|
||||
)
|
||||
caching.save_media_data(data_or_filename, mimetype, coordinates)
|
||||
else:
|
||||
# When running in "raw mode", we can't access the MediaFileManager.
|
||||
file_url = ""
|
||||
|
||||
proto.url = file_url
|
||||
|
||||
|
||||
def marshall_video(
|
||||
coordinates: str,
|
||||
proto: VideoProto,
|
||||
data: MediaData,
|
||||
mimetype: str = "video/mp4",
|
||||
start_time: int = 0,
|
||||
subtitles: SubtitleData = None,
|
||||
end_time: int | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
muted: bool = False,
|
||||
form_id: str | None = None,
|
||||
) -> None:
|
||||
"""Marshalls a video proto, using url processors as needed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates : str
|
||||
proto : the proto to fill. Must have a string field called "data".
|
||||
data : str, Path, bytes, BytesIO, numpy.ndarray, or file opened with
|
||||
io.open().
|
||||
Raw video data or a string with a URL pointing to the video
|
||||
to load. Includes support for YouTube URLs.
|
||||
If passing the raw data, this must include headers and any other
|
||||
bytes required in the actual file.
|
||||
mimetype : str
|
||||
The mime type for the video file. Defaults to 'video/mp4'.
|
||||
See https://tools.ietf.org/html/rfc4281 for more info.
|
||||
start_time : int
|
||||
The time from which this element should start playing. (default: 0)
|
||||
subtitles: str, dict, or io.BytesIO
|
||||
Optional subtitle data for the video, supporting several input types:
|
||||
- None (default): No subtitles.
|
||||
- A string: File path to a subtitle file in '.vtt' or '.srt' formats, or the raw content of subtitles conforming to these formats.
|
||||
If providing raw content, the string must adhere to the WebVTT or SRT format specifications.
|
||||
- A dictionary: Pairs of labels and file paths or raw subtitle content in '.vtt' or '.srt' formats.
|
||||
Enables multiple subtitle tracks. The label will be shown in the video player.
|
||||
Example: {'English': 'path/to/english.vtt', 'French': 'path/to/french.srt'}
|
||||
- io.BytesIO: A BytesIO stream that contains valid '.vtt' or '.srt' formatted subtitle data.
|
||||
When provided, subtitles are displayed by default. For multiple tracks, the first one is displayed by default.
|
||||
Not supported for YouTube videos.
|
||||
end_time: int
|
||||
The time at which this element should stop playing
|
||||
loop: bool
|
||||
Whether the video should loop playback.
|
||||
autoplay: bool
|
||||
Whether the video should start playing automatically.
|
||||
Browsers will not autoplay video files if the user has not interacted with
|
||||
the page yet, for example by clicking on the page while it loads.
|
||||
To enable autoplay without user interaction, you can set muted=True.
|
||||
Defaults to False.
|
||||
muted: bool
|
||||
Whether the video should play with the audio silenced. This can be used to
|
||||
enable autoplay without user interaction. Defaults to False.
|
||||
form_id: str | None
|
||||
The ID of the form that this element is placed in. Provide None if
|
||||
the element is not placed in a form.
|
||||
"""
|
||||
|
||||
if start_time < 0 or (end_time is not None and end_time <= start_time):
|
||||
raise StreamlitAPIException("Invalid start_time and end_time combination.")
|
||||
|
||||
proto.start_time = start_time
|
||||
proto.muted = muted
|
||||
|
||||
if end_time is not None:
|
||||
proto.end_time = end_time
|
||||
proto.loop = loop
|
||||
|
||||
# "type" distinguishes between YouTube and non-YouTube links
|
||||
proto.type = VideoProto.Type.NATIVE
|
||||
|
||||
if isinstance(data, Path):
|
||||
data = str(data) # Convert Path to string
|
||||
|
||||
if isinstance(data, str) and url_util.is_url(
|
||||
data, allowed_schemas=("http", "https", "data")
|
||||
):
|
||||
if youtube_url := _reshape_youtube_url(data):
|
||||
proto.url = youtube_url
|
||||
proto.type = VideoProto.Type.YOUTUBE_IFRAME
|
||||
if subtitles:
|
||||
raise StreamlitAPIException(
|
||||
"Subtitles are not supported for YouTube videos."
|
||||
)
|
||||
else:
|
||||
proto.url = data
|
||||
else:
|
||||
_marshall_av_media(coordinates, proto, data, mimetype)
|
||||
|
||||
if subtitles:
|
||||
subtitle_items: list[tuple[str, str | Path | bytes | io.BytesIO]] = []
|
||||
|
||||
# Single subtitle
|
||||
if isinstance(subtitles, (str, bytes, io.BytesIO, Path)):
|
||||
subtitle_items.append(("default", subtitles))
|
||||
# Multiple subtitles
|
||||
elif isinstance(subtitles, dict):
|
||||
subtitle_items.extend(subtitles.items())
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
f"Unsupported data type for subtitles: {type(subtitles)}. "
|
||||
f"Only str (file paths) and dict are supported."
|
||||
)
|
||||
|
||||
for label, subtitle_data in subtitle_items:
|
||||
sub = proto.subtitles.add()
|
||||
sub.label = label or ""
|
||||
|
||||
# Coordinates used in media_file_manager to identify the place of
|
||||
# element, in case of subtitle, we use same video coordinates
|
||||
# with suffix.
|
||||
# It is not aligned with common coordinates format, but in
|
||||
# media_file_manager we use it just as unique identifier, so it is fine.
|
||||
subtitle_coordinates = f"{coordinates}[subtitle{label}]"
|
||||
try:
|
||||
sub.url = process_subtitle_data(
|
||||
subtitle_coordinates, subtitle_data, label
|
||||
)
|
||||
except (TypeError, ValueError) as original_err:
|
||||
raise StreamlitAPIException(
|
||||
f"Failed to process the provided subtitle: {label}"
|
||||
) from original_err
|
||||
|
||||
if autoplay:
|
||||
proto.autoplay = autoplay
|
||||
proto.id = compute_and_register_element_id(
|
||||
"video",
|
||||
# video does not yet allow setting a user-defined key
|
||||
user_key=None,
|
||||
form_id=form_id,
|
||||
url=proto.url,
|
||||
mimetype=mimetype,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
loop=loop,
|
||||
autoplay=autoplay,
|
||||
muted=muted,
|
||||
)
|
||||
|
||||
|
||||
def _parse_start_time_end_time(
|
||||
start_time: MediaTime, end_time: MediaTime | None
|
||||
) -> tuple[int, int | None]:
|
||||
"""Parse start_time and end_time and return them as int."""
|
||||
|
||||
try:
|
||||
maybe_start_time = time_to_seconds(start_time, coerce_none_to_inf=False)
|
||||
if maybe_start_time is None:
|
||||
raise ValueError
|
||||
start_time = int(maybe_start_time)
|
||||
except (StreamlitAPIException, ValueError):
|
||||
error_msg = TIMEDELTA_PARSE_ERROR_MESSAGE.format(
|
||||
param_name="start_time", param_value=start_time
|
||||
)
|
||||
raise StreamlitAPIException(error_msg) from None
|
||||
|
||||
try:
|
||||
end_time = time_to_seconds(end_time, coerce_none_to_inf=False)
|
||||
if end_time is not None:
|
||||
end_time = int(end_time)
|
||||
except StreamlitAPIException:
|
||||
error_msg = TIMEDELTA_PARSE_ERROR_MESSAGE.format(
|
||||
param_name="end_time", param_value=end_time
|
||||
)
|
||||
raise StreamlitAPIException(error_msg) from None
|
||||
|
||||
return start_time, end_time
|
||||
|
||||
|
||||
def _validate_and_normalize(data: npt.NDArray[Any]) -> tuple[bytes, int]:
|
||||
"""Validates and normalizes numpy array data.
|
||||
We validate numpy array shape (should be 1d or 2d)
|
||||
We normalize input data to int16 [-32768, 32767] range.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : numpy array
|
||||
numpy array to be validated and normalized
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple of (bytes, int)
|
||||
(bytes, nchan)
|
||||
where
|
||||
- bytes : bytes of normalized numpy array converted to int16
|
||||
- nchan : number of channels for audio signal. 1 for mono, or 2 for stereo.
|
||||
"""
|
||||
# we import numpy here locally to import it only when needed (when numpy array given
|
||||
# to st.audio data)
|
||||
import numpy as np
|
||||
|
||||
transformed_data: npt.NDArray[Any] = np.array(data, dtype=float)
|
||||
|
||||
if len(cast("NumpyShape", transformed_data.shape)) == 1:
|
||||
nchan = 1
|
||||
elif len(transformed_data.shape) == 2:
|
||||
# In wave files,channels are interleaved. E.g.,
|
||||
# "L1R1L2R2..." for stereo. See
|
||||
# http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
|
||||
# for channel ordering
|
||||
nchan = transformed_data.shape[0]
|
||||
transformed_data = transformed_data.T.ravel()
|
||||
else:
|
||||
raise StreamlitAPIException("Numpy array audio input must be a 1D or 2D array.")
|
||||
|
||||
if transformed_data.size == 0:
|
||||
return transformed_data.astype(np.int16).tobytes(), nchan
|
||||
|
||||
max_abs_value: npt.NDArray[Any] = np.max(np.abs(transformed_data))
|
||||
# 16-bit samples are stored as 2's-complement signed integers,
|
||||
# ranging from -32768 to 32767.
|
||||
# scaled_data is PCM 16 bit numpy array, that's why we multiply [-1, 1] float
|
||||
# values to 32_767 == 2 ** 15 - 1.
|
||||
np_array = (transformed_data / max_abs_value) * 32767
|
||||
scaled_data = np_array.astype(np.int16)
|
||||
return scaled_data.tobytes(), nchan
|
||||
|
||||
|
||||
def _make_wav(data: npt.NDArray[Any], sample_rate: int) -> bytes:
|
||||
"""
|
||||
Transform a numpy array to a PCM bytestring.
|
||||
|
||||
We use code from IPython display module to convert numpy array to wave bytes
|
||||
https://github.com/ipython/ipython/blob/1015c392f3d50cf4ff3e9f29beede8c1abfdcb2a/IPython/lib/display.py#L146
|
||||
"""
|
||||
# we import wave here locally to import it only when needed (when numpy array given
|
||||
# to st.audio data)
|
||||
import wave
|
||||
|
||||
scaled, nchan = _validate_and_normalize(data)
|
||||
|
||||
with io.BytesIO() as fp, wave.open(fp, mode="wb") as waveobj:
|
||||
waveobj.setnchannels(nchan)
|
||||
waveobj.setframerate(sample_rate)
|
||||
waveobj.setsampwidth(2)
|
||||
waveobj.setcomptype("NONE", "NONE")
|
||||
waveobj.writeframes(scaled)
|
||||
return fp.getvalue()
|
||||
|
||||
|
||||
def _maybe_convert_to_wav_bytes(data: MediaData, sample_rate: int | None) -> MediaData:
|
||||
"""Convert data to wav bytes if the data type is numpy array."""
|
||||
if type_util.is_type(data, "numpy.ndarray") and sample_rate is not None:
|
||||
data = _make_wav(cast("npt.NDArray[Any]", data), sample_rate)
|
||||
return data
|
||||
|
||||
|
||||
def marshall_audio(
|
||||
coordinates: str,
|
||||
proto: AudioProto,
|
||||
data: MediaData,
|
||||
mimetype: str = "audio/wav",
|
||||
start_time: int = 0,
|
||||
sample_rate: int | None = None,
|
||||
end_time: int | None = None,
|
||||
loop: bool = False,
|
||||
autoplay: bool = False,
|
||||
form_id: str | None = None,
|
||||
) -> None:
|
||||
"""Marshalls an audio proto, using data and url processors as needed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
coordinates : str
|
||||
proto : The proto to fill. Must have a string field called "url".
|
||||
data : str, Path, bytes, BytesIO, numpy.ndarray, or file opened with
|
||||
io.open()
|
||||
Raw audio data or a string with a URL pointing to the file to load.
|
||||
If passing the raw data, this must include headers and any other bytes
|
||||
required in the actual file.
|
||||
mimetype : str
|
||||
The mime type for the audio file. Defaults to "audio/wav".
|
||||
See https://tools.ietf.org/html/rfc4281 for more info.
|
||||
start_time : int
|
||||
The time from which this element should start playing. (default: 0)
|
||||
sample_rate: int or None
|
||||
Optional param to provide sample_rate in case of numpy array
|
||||
end_time: int
|
||||
The time at which this element should stop playing
|
||||
loop: bool
|
||||
Whether the audio should loop playback.
|
||||
autoplay : bool
|
||||
Whether the audio should start playing automatically.
|
||||
Browsers will not autoplay audio files if the user has not interacted with the page yet.
|
||||
form_id: str | None
|
||||
The ID of the form that this element is placed in. Provide None if
|
||||
the element is not placed in a form.
|
||||
"""
|
||||
|
||||
proto.start_time = start_time
|
||||
if end_time is not None:
|
||||
proto.end_time = end_time
|
||||
proto.loop = loop
|
||||
|
||||
if isinstance(data, Path):
|
||||
data = str(data) # Convert Path to string
|
||||
|
||||
if isinstance(data, str) and url_util.is_url(
|
||||
data, allowed_schemas=("http", "https", "data")
|
||||
):
|
||||
proto.url = data
|
||||
else:
|
||||
data = _maybe_convert_to_wav_bytes(data, sample_rate)
|
||||
_marshall_av_media(coordinates, proto, data, mimetype)
|
||||
|
||||
if autoplay:
|
||||
proto.autoplay = autoplay
|
||||
proto.id = compute_and_register_element_id(
|
||||
"audio",
|
||||
user_key=None,
|
||||
form_id=form_id,
|
||||
url=proto.url,
|
||||
mimetype=mimetype,
|
||||
start_time=start_time,
|
||||
sample_rate=sample_rate,
|
||||
end_time=end_time,
|
||||
loop=loop,
|
||||
autoplay=autoplay,
|
||||
)
|
||||
300
myenv/lib/python3.11/site-packages/streamlit/elements/metric.py
Normal file
300
myenv/lib/python3.11/site-packages/streamlit/elements/metric.py
Normal file
@@ -0,0 +1,300 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Any, Literal, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.elements.lib.policies import maybe_raise_label_warnings
|
||||
from streamlit.elements.lib.utils import (
|
||||
LabelVisibility,
|
||||
get_label_visibility_proto_value,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Metric_pb2 import Metric as MetricProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
Value: TypeAlias = Union["np.integer[Any]", "np.floating[Any]", float, int, str, None]
|
||||
Delta: TypeAlias = Union[float, int, str, None]
|
||||
DeltaColor: TypeAlias = Literal["normal", "inverse", "off"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MetricColorAndDirection:
|
||||
color: MetricProto.MetricColor.ValueType
|
||||
direction: MetricProto.MetricDirection.ValueType
|
||||
|
||||
|
||||
class MetricMixin:
|
||||
@gather_metrics("metric")
|
||||
def metric(
|
||||
self,
|
||||
label: str,
|
||||
value: Value,
|
||||
delta: Delta = None,
|
||||
delta_color: DeltaColor = "normal",
|
||||
help: str | None = None,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
border: bool = False,
|
||||
) -> DeltaGenerator:
|
||||
r"""Display a metric in big bold font, with an optional indicator of how the metric changed.
|
||||
|
||||
Tip: If you want to display a large number, it may be a good idea to
|
||||
shorten it using packages like `millify <https://github.com/azaitsev/millify>`_
|
||||
or `numerize <https://github.com/davidsa03/numerize>`_. E.g. ``1234`` can be
|
||||
displayed as ``1.2k`` using ``st.metric("Short number", millify(1234))``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The header or title for the metric. The label can optionally
|
||||
contain GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code, Links, and Images. Images display like
|
||||
icons, with a max height equal to the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
value : int, float, str, or None
|
||||
Value of the metric. None is rendered as a long dash.
|
||||
|
||||
delta : int, float, str, or None
|
||||
Indicator of how the metric changed, rendered with an arrow below
|
||||
the metric. If delta is negative (int/float) or starts with a minus
|
||||
sign (str), the arrow points down and the text is red; else the
|
||||
arrow points up and the text is green. If None (default), no delta
|
||||
indicator is shown.
|
||||
|
||||
delta_color : "normal", "inverse", or "off"
|
||||
If "normal" (default), the delta indicator is shown as described
|
||||
above. If "inverse", it is red when positive and green when
|
||||
negative. This is useful when a negative change is considered
|
||||
good, e.g. if cost decreased. If "off", delta is shown in gray
|
||||
regardless of its value.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the metric label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
border : bool
|
||||
Whether to show a border around the metric container. If this is
|
||||
``False`` (default), no border is shown. If this is ``True``, a
|
||||
border is shown.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Show a metric**
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.metric(label="Temperature", value="70 °F", delta="1.2 °F")
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example1.streamlit.app/
|
||||
height: 210px
|
||||
|
||||
**Example 2: Create a row of metrics**
|
||||
|
||||
``st.metric`` looks especially nice in combination with ``st.columns``.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> col1, col2, col3 = st.columns(3)
|
||||
>>> col1.metric("Temperature", "70 °F", "1.2 °F")
|
||||
>>> col2.metric("Wind", "9 mph", "-8%")
|
||||
>>> col3.metric("Humidity", "86%", "4%")
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example2.streamlit.app/
|
||||
height: 210px
|
||||
|
||||
**Example 3: Modify the delta indicator**
|
||||
|
||||
The delta indicator color can also be inverted or turned off.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.metric(label="Gas price", value=4, delta=-0.5, delta_color="inverse")
|
||||
>>>
|
||||
>>> st.metric(
|
||||
... label="Active developers", value=123, delta=123, delta_color="off"
|
||||
... )
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example3.streamlit.app/
|
||||
height: 320px
|
||||
|
||||
**Example 4: Create a grid of metric cards**
|
||||
|
||||
Add borders to your metrics to create a dashboard look.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> a, b = st.columns(2)
|
||||
>>> c, d = st.columns(2)
|
||||
>>>
|
||||
>>> a.metric("Temperature", "30°F", "-9°F", border=True)
|
||||
>>> b.metric("Wind", "4 mph", "2 mph", border=True)
|
||||
>>>
|
||||
>>> c.metric("Humidity", "77%", "5%", border=True)
|
||||
>>> d.metric("Pressure", "30.34 inHg", "-2 inHg", border=True)
|
||||
|
||||
.. output::
|
||||
https://doc-metric-example4.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
metric_proto = MetricProto()
|
||||
metric_proto.body = _parse_value(value)
|
||||
metric_proto.label = _parse_label(label)
|
||||
metric_proto.delta = _parse_delta(delta)
|
||||
metric_proto.show_border = border
|
||||
if help is not None:
|
||||
metric_proto.help = dedent(help)
|
||||
|
||||
color_and_direction = _determine_delta_color_and_direction(
|
||||
cast("DeltaColor", clean_text(delta_color)), delta
|
||||
)
|
||||
metric_proto.color = color_and_direction.color
|
||||
metric_proto.direction = color_and_direction.direction
|
||||
metric_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
return self.dg._enqueue("metric", metric_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def _parse_label(label: str) -> str:
|
||||
if not isinstance(label, str):
|
||||
raise TypeError(
|
||||
f"'{str(label)}' is of type {str(type(label))}, which is not an accepted type."
|
||||
" label only accepts: str. Please convert the label to an accepted type."
|
||||
)
|
||||
return label
|
||||
|
||||
|
||||
def _parse_value(value: Value) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
if isinstance(value, int) or isinstance(value, float) or isinstance(value, str):
|
||||
return str(value)
|
||||
elif hasattr(value, "item"):
|
||||
# Add support for numpy values (e.g. int16, float64, etc.)
|
||||
try:
|
||||
# Item could also be just a variable, so we use try, except
|
||||
if isinstance(value.item(), float) or isinstance(value.item(), int):
|
||||
return str(value.item())
|
||||
except Exception:
|
||||
# If the numpy item is not a valid value, the TypeError below will be raised.
|
||||
pass
|
||||
|
||||
raise TypeError(
|
||||
f"'{str(value)}' is of type {str(type(value))}, which is not an accepted type."
|
||||
" value only accepts: int, float, str, or None."
|
||||
" Please convert the value to an accepted type."
|
||||
)
|
||||
|
||||
|
||||
def _parse_delta(delta: Delta) -> str:
|
||||
if delta is None or delta == "":
|
||||
return ""
|
||||
if isinstance(delta, str):
|
||||
return dedent(delta)
|
||||
elif isinstance(delta, int) or isinstance(delta, float):
|
||||
return str(delta)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"'{str(delta)}' is of type {str(type(delta))}, which is not an accepted type."
|
||||
" delta only accepts: int, float, str, or None."
|
||||
" Please convert the value to an accepted type."
|
||||
)
|
||||
|
||||
|
||||
def _determine_delta_color_and_direction(
|
||||
delta_color: DeltaColor,
|
||||
delta: Delta,
|
||||
) -> MetricColorAndDirection:
|
||||
if delta_color not in {"normal", "inverse", "off"}:
|
||||
raise StreamlitAPIException(
|
||||
f"'{str(delta_color)}' is not an accepted value. delta_color only accepts: "
|
||||
"'normal', 'inverse', or 'off'"
|
||||
)
|
||||
|
||||
if delta is None or delta == "":
|
||||
return MetricColorAndDirection(
|
||||
color=MetricProto.MetricColor.GRAY,
|
||||
direction=MetricProto.MetricDirection.NONE,
|
||||
)
|
||||
|
||||
if _is_negative_delta(delta):
|
||||
if delta_color == "normal":
|
||||
cd_color = MetricProto.MetricColor.RED
|
||||
elif delta_color == "inverse":
|
||||
cd_color = MetricProto.MetricColor.GREEN
|
||||
else:
|
||||
cd_color = MetricProto.MetricColor.GRAY
|
||||
cd_direction = MetricProto.MetricDirection.DOWN
|
||||
else:
|
||||
if delta_color == "normal":
|
||||
cd_color = MetricProto.MetricColor.GREEN
|
||||
elif delta_color == "inverse":
|
||||
cd_color = MetricProto.MetricColor.RED
|
||||
else:
|
||||
cd_color = MetricProto.MetricColor.GRAY
|
||||
cd_direction = MetricProto.MetricDirection.UP
|
||||
|
||||
return MetricColorAndDirection(
|
||||
color=cd_color,
|
||||
direction=cd_direction,
|
||||
)
|
||||
|
||||
|
||||
def _is_negative_delta(delta: Delta) -> bool:
|
||||
return dedent(str(delta)).startswith("-")
|
||||
@@ -0,0 +1,546 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Streamlit support for Plotly charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import type_util
|
||||
from streamlit.deprecation_util import show_deprecation_warning
|
||||
from streamlit.elements.lib.event_utils import AttributeDictionary
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import check_widget_policies
|
||||
from streamlit.elements.lib.streamlit_plotly_theme import (
|
||||
configure_streamlit_plotly_theme,
|
||||
)
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.PlotlyChart_pb2 import PlotlyChart as PlotlyChartProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.runtime.state import WidgetCallback, register_widget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
import matplotlib as mpl
|
||||
import plotly.graph_objs as go
|
||||
from plotly.basedatatypes import BaseFigure
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
# We need to configure the Plotly theme before any Plotly figures are created:
|
||||
configure_streamlit_plotly_theme()
|
||||
|
||||
_AtomicFigureOrData: TypeAlias = Union[
|
||||
"go.Figure",
|
||||
"go.Data",
|
||||
]
|
||||
FigureOrData: TypeAlias = Union[
|
||||
_AtomicFigureOrData,
|
||||
list[_AtomicFigureOrData],
|
||||
# It is kind of hard to figure out exactly what kind of dict is supported
|
||||
# here, as plotly hasn't embraced typing yet. This version is chosen to
|
||||
# align with the docstring.
|
||||
dict[str, _AtomicFigureOrData],
|
||||
"BaseFigure",
|
||||
"mpl.figure.Figure",
|
||||
]
|
||||
|
||||
SelectionMode: TypeAlias = Literal["lasso", "points", "box"]
|
||||
_SELECTION_MODES: Final[set[SelectionMode]] = {"lasso", "points", "box"}
|
||||
|
||||
|
||||
class PlotlySelectionState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the Plotly chart selection state.
|
||||
|
||||
The selection state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Selection states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
points : list[dict[str, Any]]
|
||||
The selected data points in the chart, including the data points
|
||||
selected by the box and lasso mode. The data includes the values
|
||||
associated to each point and a point index used to populate
|
||||
``point_indices``. If additional information has been assigned to your
|
||||
points, such as size or legend group, this is also included.
|
||||
|
||||
point_indices : list[int]
|
||||
The numerical indices of all selected data points in the chart. The
|
||||
details of each identified point are included in ``points``.
|
||||
|
||||
box : list[dict[str, Any]]
|
||||
The metadata related to the box selection. This includes the
|
||||
coordinates of the selected area.
|
||||
|
||||
lasso : list[dict[str, Any]]
|
||||
The metadata related to the lasso selection. This includes the
|
||||
coordinates of the selected area.
|
||||
|
||||
Example
|
||||
-------
|
||||
When working with more complicated graphs, the ``points`` attribute
|
||||
displays additional information. Try selecting points in the following
|
||||
example:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import plotly.express as px
|
||||
>>>
|
||||
>>> df = px.data.iris()
|
||||
>>> fig = px.scatter(
|
||||
... df,
|
||||
... x="sepal_width",
|
||||
... y="sepal_length",
|
||||
... color="species",
|
||||
... size="petal_length",
|
||||
... hover_data=["petal_width"],
|
||||
... )
|
||||
>>>
|
||||
>>> event = st.plotly_chart(fig, key="iris", on_select="rerun")
|
||||
>>>
|
||||
>>> event.selection
|
||||
|
||||
.. output::
|
||||
https://doc-chart-events-plotly-selection-state.streamlit.app
|
||||
height: 600px
|
||||
|
||||
This is an example of the selection state when selecting a single point:
|
||||
|
||||
>>> {
|
||||
>>> "points": [
|
||||
>>> {
|
||||
>>> "curve_number": 2,
|
||||
>>> "point_number": 9,
|
||||
>>> "point_index": 9,
|
||||
>>> "x": 3.6,
|
||||
>>> "y": 7.2,
|
||||
>>> "customdata": [
|
||||
>>> 2.5
|
||||
>>> ],
|
||||
>>> "marker_size": 6.1,
|
||||
>>> "legendgroup": "virginica"
|
||||
>>> }
|
||||
>>> ],
|
||||
>>> "point_indices": [
|
||||
>>> 9
|
||||
>>> ],
|
||||
>>> "box": [],
|
||||
>>> "lasso": []
|
||||
>>> }
|
||||
|
||||
"""
|
||||
|
||||
points: list[dict[str, Any]]
|
||||
point_indices: list[int]
|
||||
box: list[dict[str, Any]]
|
||||
lasso: list[dict[str, Any]]
|
||||
|
||||
|
||||
class PlotlyState(TypedDict, total=False):
|
||||
"""
|
||||
The schema for the Plotly chart event state.
|
||||
|
||||
The event state is stored in a dictionary-like object that supports both
|
||||
key and attribute notation. Event states cannot be programmatically
|
||||
changed or set through Session State.
|
||||
|
||||
Only selection events are supported at this time.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
selection : dict
|
||||
The state of the ``on_select`` event. This attribute returns a
|
||||
dictionary-like object that supports both key and attribute notation.
|
||||
The attributes are described by the ``PlotlySelectionState`` dictionary
|
||||
schema.
|
||||
|
||||
Example
|
||||
-------
|
||||
Try selecting points by any of the three available methods (direct click,
|
||||
box, or lasso). The current selection state is available through Session
|
||||
State or as the output of the chart function.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import plotly.express as px
|
||||
>>>
|
||||
>>> df = px.data.iris() # iris is a pandas DataFrame
|
||||
>>> fig = px.scatter(df, x="sepal_width", y="sepal_length")
|
||||
>>>
|
||||
>>> event = st.plotly_chart(fig, key="iris", on_select="rerun")
|
||||
>>>
|
||||
>>> event
|
||||
|
||||
.. output::
|
||||
https://doc-chart-events-plotly-state.streamlit.app
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
|
||||
selection: PlotlySelectionState
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlotlyChartSelectionSerde:
|
||||
"""PlotlyChartSelectionSerde is used to serialize and deserialize the Plotly Chart
|
||||
selection state.
|
||||
"""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> PlotlyState:
|
||||
empty_selection_state: PlotlyState = {
|
||||
"selection": {
|
||||
"points": [],
|
||||
"point_indices": [],
|
||||
"box": [],
|
||||
"lasso": [],
|
||||
},
|
||||
}
|
||||
|
||||
selection_state = (
|
||||
empty_selection_state
|
||||
if ui_value is None
|
||||
else cast("PlotlyState", AttributeDictionary(json.loads(ui_value)))
|
||||
)
|
||||
|
||||
if "selection" not in selection_state:
|
||||
selection_state = empty_selection_state
|
||||
|
||||
return cast("PlotlyState", AttributeDictionary(selection_state))
|
||||
|
||||
def serialize(self, selection_state: PlotlyState) -> str:
|
||||
return json.dumps(selection_state, default=str)
|
||||
|
||||
|
||||
def parse_selection_mode(
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode],
|
||||
) -> set[PlotlyChartProto.SelectionMode.ValueType]:
|
||||
"""Parse and check the user provided selection modes."""
|
||||
if isinstance(selection_mode, str):
|
||||
# Only a single selection mode was passed
|
||||
selection_mode_set = {selection_mode}
|
||||
else:
|
||||
# Multiple selection modes were passed
|
||||
selection_mode_set = set(selection_mode)
|
||||
|
||||
if not selection_mode_set.issubset(_SELECTION_MODES):
|
||||
raise StreamlitAPIException(
|
||||
f"Invalid selection mode: {selection_mode}. "
|
||||
f"Valid options are: {_SELECTION_MODES}"
|
||||
)
|
||||
|
||||
parsed_selection_modes = []
|
||||
for selection_mode in selection_mode_set:
|
||||
if selection_mode == "points":
|
||||
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.POINTS)
|
||||
elif selection_mode == "lasso":
|
||||
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.LASSO)
|
||||
elif selection_mode == "box":
|
||||
parsed_selection_modes.append(PlotlyChartProto.SelectionMode.BOX)
|
||||
return set(parsed_selection_modes)
|
||||
|
||||
|
||||
class PlotlyMixin:
|
||||
@overload
|
||||
def plotly_chart(
|
||||
self,
|
||||
figure_or_data: FigureOrData,
|
||||
use_container_width: bool = True,
|
||||
*,
|
||||
theme: Literal["streamlit"] | None = "streamlit",
|
||||
key: Key | None = None,
|
||||
on_select: Literal["ignore"], # No default value here to make it work with mypy
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
||||
"points",
|
||||
"box",
|
||||
"lasso",
|
||||
),
|
||||
**kwargs: Any,
|
||||
) -> DeltaGenerator: ...
|
||||
|
||||
@overload
|
||||
def plotly_chart(
|
||||
self,
|
||||
figure_or_data: FigureOrData,
|
||||
use_container_width: bool = True,
|
||||
*,
|
||||
theme: Literal["streamlit"] | None = "streamlit",
|
||||
key: Key | None = None,
|
||||
on_select: Literal["rerun"] | WidgetCallback = "rerun",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
||||
"points",
|
||||
"box",
|
||||
"lasso",
|
||||
),
|
||||
**kwargs: Any,
|
||||
) -> PlotlyState: ...
|
||||
|
||||
@gather_metrics("plotly_chart")
|
||||
def plotly_chart(
|
||||
self,
|
||||
figure_or_data: FigureOrData,
|
||||
use_container_width: bool = True,
|
||||
*,
|
||||
theme: Literal["streamlit"] | None = "streamlit",
|
||||
key: Key | None = None,
|
||||
on_select: Literal["rerun", "ignore"] | WidgetCallback = "ignore",
|
||||
selection_mode: SelectionMode | Iterable[SelectionMode] = (
|
||||
"points",
|
||||
"box",
|
||||
"lasso",
|
||||
),
|
||||
**kwargs: Any,
|
||||
) -> DeltaGenerator | PlotlyState:
|
||||
"""Display an interactive Plotly chart.
|
||||
|
||||
`Plotly <https://plot.ly/python>`_ is a charting library for Python.
|
||||
The arguments to this function closely follow the ones for Plotly's
|
||||
``plot()`` function.
|
||||
|
||||
To show Plotly charts in Streamlit, call ``st.plotly_chart`` wherever
|
||||
you would call Plotly's ``py.plot`` or ``py.iplot``.
|
||||
|
||||
.. Important::
|
||||
You must install ``plotly`` to use this command. Your app's
|
||||
performance may be enhanced by installing ``orjson`` as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
figure_or_data : plotly.graph_objs.Figure, plotly.graph_objs.Data,\
|
||||
or dict/list of plotly.graph_objs.Figure/Data
|
||||
|
||||
The Plotly ``Figure`` or ``Data`` object to render. See
|
||||
https://plot.ly/python/ for examples of graph descriptions.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True`` (default),
|
||||
Streamlit sets the width of the figure to match the width of the parent
|
||||
container. If ``use_container_width`` is ``False``, Streamlit sets the
|
||||
width of the chart to fit its contents according to the plotting library,
|
||||
up to the width of the parent container.
|
||||
|
||||
theme : "streamlit" or None
|
||||
The theme of the chart. If ``theme`` is ``"streamlit"`` (default),
|
||||
Streamlit uses its own design default. If ``theme`` is ``None``,
|
||||
Streamlit falls back to the default behavior of the library.
|
||||
|
||||
key : str
|
||||
An optional string to use for giving this element a stable
|
||||
identity. If ``key`` is ``None`` (default), this element's identity
|
||||
will be determined based on the values of the other parameters.
|
||||
|
||||
Additionally, if selections are activated and ``key`` is provided,
|
||||
Streamlit will register the key in Session State to store the
|
||||
selection state. The selection state is read-only.
|
||||
|
||||
on_select : "ignore" or "rerun" or callable
|
||||
How the figure should respond to user selection events. This
|
||||
controls whether or not the figure behaves like an input widget.
|
||||
``on_select`` can be one of the following:
|
||||
|
||||
- ``"ignore"`` (default): Streamlit will not react to any selection
|
||||
events in the chart. The figure will not behave like an input
|
||||
widget.
|
||||
|
||||
- ``"rerun"``: Streamlit will rerun the app when the user selects
|
||||
data in the chart. In this case, ``st.plotly_chart`` will return
|
||||
the selection data as a dictionary.
|
||||
|
||||
- A ``callable``: Streamlit will rerun the app and execute the
|
||||
``callable`` as a callback function before the rest of the app.
|
||||
In this case, ``st.plotly_chart`` will return the selection data
|
||||
as a dictionary.
|
||||
|
||||
selection_mode : "points", "box", "lasso" or an Iterable of these
|
||||
The selection mode of the chart. This can be one of the following:
|
||||
|
||||
- ``"points"``: The chart will allow selections based on individual
|
||||
data points.
|
||||
- ``"box"``: The chart will allow selections based on rectangular
|
||||
areas.
|
||||
- ``"lasso"``: The chart will allow selections based on freeform
|
||||
areas.
|
||||
- An ``Iterable`` of the above options: The chart will allow
|
||||
selections based on the modes specified.
|
||||
|
||||
All selections modes are activated by default.
|
||||
|
||||
**kwargs
|
||||
Any argument accepted by Plotly's ``plot()`` function.
|
||||
|
||||
Returns
|
||||
-------
|
||||
element or dict
|
||||
If ``on_select`` is ``"ignore"`` (default), this command returns an
|
||||
internal placeholder for the chart element. Otherwise, this command
|
||||
returns a dictionary-like object that supports both key and
|
||||
attribute notation. The attributes are described by the
|
||||
``PlotlyState`` dictionary schema.
|
||||
|
||||
Example
|
||||
-------
|
||||
The example below comes straight from the examples at
|
||||
https://plot.ly/python. Note that ``plotly.figure_factory`` requires
|
||||
``scipy`` to run.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>> import plotly.figure_factory as ff
|
||||
>>>
|
||||
>>> # Add histogram data
|
||||
>>> x1 = np.random.randn(200) - 2
|
||||
>>> x2 = np.random.randn(200)
|
||||
>>> x3 = np.random.randn(200) + 2
|
||||
>>>
|
||||
>>> # Group data together
|
||||
>>> hist_data = [x1, x2, x3]
|
||||
>>>
|
||||
>>> group_labels = ['Group 1', 'Group 2', 'Group 3']
|
||||
>>>
|
||||
>>> # Create distplot with custom bin_size
|
||||
>>> fig = ff.create_distplot(
|
||||
... hist_data, group_labels, bin_size=[.1, .25, .5])
|
||||
>>>
|
||||
>>> # Plot!
|
||||
>>> st.plotly_chart(fig)
|
||||
|
||||
.. output::
|
||||
https://doc-plotly-chart.streamlit.app/
|
||||
height: 550px
|
||||
|
||||
"""
|
||||
import plotly.io
|
||||
import plotly.tools
|
||||
|
||||
# NOTE: "figure_or_data" is the name used in Plotly's .plot() method
|
||||
# for their main parameter. I don't like the name, but it's best to
|
||||
# keep it in sync with what Plotly calls it.
|
||||
|
||||
if "sharing" in kwargs:
|
||||
show_deprecation_warning(
|
||||
"The `sharing` parameter has been deprecated and will be removed "
|
||||
"in a future release. Plotly charts will always be rendered using "
|
||||
"Streamlit's offline mode."
|
||||
)
|
||||
|
||||
if theme not in ["streamlit", None]:
|
||||
raise StreamlitAPIException(
|
||||
f'You set theme="{theme}" while Streamlit charts only support '
|
||||
"theme=”streamlit” or theme=None to fallback to the default "
|
||||
"library theme."
|
||||
)
|
||||
|
||||
if on_select not in ["ignore", "rerun"] and not callable(on_select):
|
||||
raise StreamlitAPIException(
|
||||
f"You have passed {on_select} to `on_select`. But only 'ignore', "
|
||||
"'rerun', or a callable is supported."
|
||||
)
|
||||
|
||||
key = to_key(key)
|
||||
is_selection_activated = on_select != "ignore"
|
||||
|
||||
if is_selection_activated:
|
||||
# Run some checks that are only relevant when selections are activated
|
||||
|
||||
is_callback = callable(on_select)
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change=cast("WidgetCallback", on_select) if is_callback else None,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
enable_check_callback_rules=is_callback,
|
||||
)
|
||||
|
||||
if type_util.is_type(figure_or_data, "matplotlib.figure.Figure"):
|
||||
# Convert matplotlib figure to plotly figure:
|
||||
figure = plotly.tools.mpl_to_plotly(figure_or_data)
|
||||
else:
|
||||
figure = plotly.tools.return_figure_from_figure_or_data(
|
||||
figure_or_data, validate_figure=True
|
||||
)
|
||||
|
||||
plotly_chart_proto = PlotlyChartProto()
|
||||
plotly_chart_proto.use_container_width = use_container_width
|
||||
plotly_chart_proto.theme = theme or ""
|
||||
plotly_chart_proto.form_id = current_form_id(self.dg)
|
||||
|
||||
config = dict(kwargs.get("config", {}))
|
||||
# Copy over some kwargs to config dict. Plotly does the same in plot().
|
||||
config.setdefault("showLink", kwargs.get("show_link", False))
|
||||
config.setdefault("linkText", kwargs.get("link_text", False))
|
||||
|
||||
plotly_chart_proto.spec = plotly.io.to_json(figure, validate=False)
|
||||
plotly_chart_proto.config = json.dumps(config)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
# We are computing the widget id for all plotly uses
|
||||
# to also allow non-widget Plotly charts to keep their state
|
||||
# when the frontend component gets unmounted and remounted.
|
||||
plotly_chart_proto.id = compute_and_register_element_id(
|
||||
"plotly_chart",
|
||||
user_key=key,
|
||||
form_id=plotly_chart_proto.form_id,
|
||||
plotly_spec=plotly_chart_proto.spec,
|
||||
plotly_config=plotly_chart_proto.config,
|
||||
selection_mode=selection_mode,
|
||||
is_selection_activated=is_selection_activated,
|
||||
theme=theme,
|
||||
use_container_width=use_container_width,
|
||||
)
|
||||
|
||||
if is_selection_activated:
|
||||
# Selections are activated, treat plotly chart as a widget:
|
||||
plotly_chart_proto.selection_mode.extend(
|
||||
parse_selection_mode(selection_mode)
|
||||
)
|
||||
|
||||
serde = PlotlyChartSelectionSerde()
|
||||
|
||||
widget_state = register_widget(
|
||||
plotly_chart_proto.id,
|
||||
on_change_handler=on_select if callable(on_select) else None,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="string_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("plotly_chart", plotly_chart_proto)
|
||||
return cast("PlotlyState", widget_state.value)
|
||||
else:
|
||||
return self.dg._enqueue("plotly_chart", plotly_chart_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,156 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Progress_pb2 import Progress as ProgressProto
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
# Currently, equates to just float, but we can't use `numbers.Real` due to
|
||||
# https://github.com/python/mypy/issues/3186
|
||||
FloatOrInt: TypeAlias = Union[int, float]
|
||||
|
||||
|
||||
def _check_float_between(value: float, low: float = 0.0, high: float = 1.0) -> bool:
|
||||
"""
|
||||
Checks given value is 'between' the bounds of [low, high],
|
||||
considering close values around bounds are acceptable input.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This check is required for handling values that are slightly above or below the
|
||||
acceptable range, for example -0.0000000000021, 1.0000000000000013.
|
||||
These values are little off the conventional 0.0 <= x <= 1.0 condition
|
||||
due to floating point operations, but should still be considered acceptable input.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
low : float
|
||||
high : float
|
||||
|
||||
"""
|
||||
return (
|
||||
(low <= value <= high)
|
||||
or math.isclose(value, low, rel_tol=1e-9, abs_tol=1e-9)
|
||||
or math.isclose(value, high, rel_tol=1e-9, abs_tol=1e-9)
|
||||
)
|
||||
|
||||
|
||||
def _get_value(value):
|
||||
if isinstance(value, int):
|
||||
if 0 <= value <= 100:
|
||||
return value
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Progress Value has invalid value [0, 100]: %d" % value
|
||||
)
|
||||
|
||||
elif isinstance(value, float):
|
||||
if _check_float_between(value, low=0.0, high=1.0):
|
||||
return int(value * 100)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Progress Value has invalid value [0.0, 1.0]: %f" % value
|
||||
)
|
||||
else:
|
||||
raise StreamlitAPIException(
|
||||
"Progress Value has invalid type: %s" % type(value).__name__
|
||||
)
|
||||
|
||||
|
||||
def _get_text(text: str | None) -> str | None:
|
||||
if text is None:
|
||||
return None
|
||||
if isinstance(text, str):
|
||||
return clean_text(text)
|
||||
raise StreamlitAPIException(
|
||||
f"Progress Text is of type {str(type(text))}, which is not an accepted type."
|
||||
"Text only accepts: str. Please convert the text to an accepted type."
|
||||
)
|
||||
|
||||
|
||||
class ProgressMixin:
|
||||
def progress(self, value: FloatOrInt, text: str | None = None) -> DeltaGenerator:
|
||||
r"""Display a progress bar.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : int or float
|
||||
0 <= value <= 100 for int
|
||||
|
||||
0.0 <= value <= 1.0 for float
|
||||
|
||||
text : str or None
|
||||
A message to display above the progress bar. The text can optionally
|
||||
contain GitHub-flavored Markdown of the following types: Bold, Italics,
|
||||
Strikethroughs, Inline Code, Links, and Images. Images display like
|
||||
icons, with a max height equal to the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
Example
|
||||
-------
|
||||
Here is an example of a progress bar increasing over time and disappearing when it reaches completion:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> progress_text = "Operation in progress. Please wait."
|
||||
>>> my_bar = st.progress(0, text=progress_text)
|
||||
>>>
|
||||
>>> for percent_complete in range(100):
|
||||
... time.sleep(0.01)
|
||||
... my_bar.progress(percent_complete + 1, text=progress_text)
|
||||
>>> time.sleep(1)
|
||||
>>> my_bar.empty()
|
||||
>>>
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output::
|
||||
https://doc-status-progress.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
# TODO: standardize numerical type checking across st.* functions.
|
||||
progress_proto = ProgressProto()
|
||||
progress_proto.value = _get_value(value)
|
||||
text = _get_text(text)
|
||||
if text is not None:
|
||||
progress_proto.text = text
|
||||
return self.dg._enqueue("progress", progress_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
194
myenv/lib/python3.11/site-packages/streamlit/elements/pyplot.py
Normal file
194
myenv/lib/python3.11/site-packages/streamlit/elements/pyplot.py
Normal file
@@ -0,0 +1,194 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Streamlit support for Matplotlib PyPlot charts."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from streamlit.deprecation_util import show_deprecation_warning
|
||||
from streamlit.elements.lib.image_utils import WidthBehavior, marshall_images
|
||||
from streamlit.proto.Image_pb2 import ImageList as ImageListProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from matplotlib.figure import Figure
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class PyplotMixin:
|
||||
@gather_metrics("pyplot")
|
||||
def pyplot(
|
||||
self,
|
||||
fig: Figure | None = None,
|
||||
clear_figure: bool | None = None,
|
||||
use_container_width: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a matplotlib.pyplot figure.
|
||||
|
||||
.. Important::
|
||||
You must install ``matplotlib`` to use this command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
fig : Matplotlib Figure
|
||||
The Matplotlib ``Figure`` object to render. See
|
||||
https://matplotlib.org/stable/gallery/index.html for examples.
|
||||
|
||||
.. note::
|
||||
When this argument isn't specified, this function will render the global
|
||||
Matplotlib figure object. However, this feature is deprecated and
|
||||
will be removed in a later version.
|
||||
|
||||
clear_figure : bool
|
||||
If True, the figure will be cleared after being rendered.
|
||||
If False, the figure will not be cleared after being rendered.
|
||||
If left unspecified, we pick a default based on the value of ``fig``.
|
||||
|
||||
- If ``fig`` is set, defaults to ``False``.
|
||||
|
||||
- If ``fig`` is not set, defaults to ``True``. This simulates Jupyter's
|
||||
approach to matplotlib rendering.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override the figure's native width with the width of
|
||||
the parent container. If ``use_container_width`` is ``True``
|
||||
(default), Streamlit sets the width of the figure to match the
|
||||
width of the parent container. If ``use_container_width`` is
|
||||
``False``, Streamlit sets the width of the chart to fit its
|
||||
contents according to the plotting library, up to the width of the
|
||||
parent container.
|
||||
|
||||
**kwargs : any
|
||||
Arguments to pass to Matplotlib's savefig function.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import matplotlib.pyplot as plt
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> arr = np.random.normal(1, 1, size=100)
|
||||
>>> fig, ax = plt.subplots()
|
||||
>>> ax.hist(arr, bins=20)
|
||||
>>>
|
||||
>>> st.pyplot(fig)
|
||||
|
||||
.. output::
|
||||
https://doc-pyplot.streamlit.app/
|
||||
height: 630px
|
||||
|
||||
Matplotlib supports several types of "backends". If you're getting an
|
||||
error using Matplotlib with Streamlit, try setting your backend to "TkAgg"::
|
||||
|
||||
echo "backend: TkAgg" >> ~/.matplotlib/matplotlibrc
|
||||
|
||||
For more information, see https://matplotlib.org/faq/usage_faq.html.
|
||||
|
||||
"""
|
||||
|
||||
if not fig:
|
||||
show_deprecation_warning("""
|
||||
Calling `st.pyplot()` without providing a figure argument has been deprecated
|
||||
and will be removed in a later version as it requires the use of Matplotlib's
|
||||
global figure object, which is not thread-safe.
|
||||
|
||||
To future-proof this code, you should pass in a figure as shown below:
|
||||
|
||||
```python
|
||||
fig, ax = plt.subplots()
|
||||
ax.scatter([1, 2, 3], [1, 2, 3])
|
||||
# other plotting actions...
|
||||
st.pyplot(fig)
|
||||
```
|
||||
|
||||
If you have a specific use case that requires this functionality, please let us
|
||||
know via [issue on Github](https://github.com/streamlit/streamlit/issues).
|
||||
""")
|
||||
|
||||
image_list_proto = ImageListProto()
|
||||
marshall(
|
||||
self.dg._get_delta_path_str(),
|
||||
image_list_proto,
|
||||
fig,
|
||||
clear_figure,
|
||||
use_container_width,
|
||||
**kwargs,
|
||||
)
|
||||
return self.dg._enqueue("imgs", image_list_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
|
||||
|
||||
def marshall(
|
||||
coordinates: str,
|
||||
image_list_proto: ImageListProto,
|
||||
fig: Figure | None = None,
|
||||
clear_figure: bool | None = True,
|
||||
use_container_width: bool = True,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
plt.ioff()
|
||||
except ImportError:
|
||||
raise ImportError("pyplot() command requires matplotlib")
|
||||
|
||||
# You can call .savefig() on a Figure object or directly on the pyplot
|
||||
# module, in which case you're doing it to the latest Figure.
|
||||
if not fig:
|
||||
if clear_figure is None:
|
||||
clear_figure = True
|
||||
|
||||
fig = cast("Figure", plt)
|
||||
|
||||
# Normally, dpi is set to 'figure', and the figure's dpi is set to 100.
|
||||
# So here we pick double of that to make things look good in a high
|
||||
# DPI display.
|
||||
options = {"bbox_inches": "tight", "dpi": 200, "format": "png"}
|
||||
|
||||
# If some options are passed in from kwargs then replace the values in
|
||||
# options with the ones from kwargs
|
||||
options = {a: kwargs.get(a, b) for a, b in options.items()}
|
||||
# Merge options back into kwargs.
|
||||
kwargs.update(options)
|
||||
|
||||
image = io.BytesIO()
|
||||
fig.savefig(image, **kwargs)
|
||||
image_width = (
|
||||
WidthBehavior.COLUMN if use_container_width else WidthBehavior.ORIGINAL
|
||||
)
|
||||
marshall_images(
|
||||
coordinates=coordinates,
|
||||
image=image,
|
||||
caption=None,
|
||||
width=image_width,
|
||||
proto_imgs=image_list_proto,
|
||||
clamp=False,
|
||||
channels="RGB",
|
||||
output_format="PNG",
|
||||
)
|
||||
|
||||
# Clear the figure after rendering it. This means that subsequent
|
||||
# plt calls will be starting fresh.
|
||||
if clear_figure:
|
||||
fig.clf()
|
||||
@@ -0,0 +1,47 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Snow_pb2 import Snow as SnowProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
class SnowMixin:
|
||||
@gather_metrics("snow")
|
||||
def snow(self) -> DeltaGenerator:
|
||||
"""Draw celebratory snowfall.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.snow()
|
||||
|
||||
...then watch your app and get ready for a cool celebration!
|
||||
|
||||
"""
|
||||
snow_proto = SnowProto()
|
||||
snow_proto.show = True
|
||||
return self.dg._enqueue("snow", snow_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
113
myenv/lib/python3.11/site-packages/streamlit/elements/spinner.py
Normal file
113
myenv/lib/python3.11/site-packages/streamlit/elements/spinner.py
Normal file
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import streamlit as st
|
||||
from streamlit.runtime.scriptrunner import add_script_run_ctx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def spinner(
|
||||
text: str = "In progress...",
|
||||
*,
|
||||
show_time: bool = False,
|
||||
_cache: bool = False,
|
||||
) -> Iterator[None]:
|
||||
"""Display a loading spinner while executing a block of code.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
text : str
|
||||
The text to display next to the spinner. This defaults to
|
||||
``"In progress..."``.
|
||||
|
||||
The text can optionally contain GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional, supported
|
||||
Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
show_time : bool
|
||||
Whether to show the elapsed time next to the spinner text. If this is
|
||||
``False`` (default), no time is displayed. If this is ``True``,
|
||||
elapsed time is displayed with a precision of 0.1 seconds. The time
|
||||
format is not configurable.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>> import time
|
||||
>>>
|
||||
>>> with st.spinner("Wait for it...", show_time=True):
|
||||
>>> time.sleep(5)
|
||||
>>> st.success("Done!")
|
||||
>>> st.button("Rerun")
|
||||
|
||||
.. output ::
|
||||
https://doc-spinner.streamlit.app/
|
||||
height: 210px
|
||||
|
||||
"""
|
||||
from streamlit.proto.Spinner_pb2 import Spinner as SpinnerProto
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
message = st.empty()
|
||||
|
||||
# Set the message 0.5 seconds in the future to avoid annoying
|
||||
# flickering if this spinner runs too quickly.
|
||||
DELAY_SECS = 0.5
|
||||
display_message = True
|
||||
display_message_lock = threading.Lock()
|
||||
|
||||
try:
|
||||
|
||||
def set_message():
|
||||
with display_message_lock:
|
||||
if display_message:
|
||||
spinner_proto = SpinnerProto()
|
||||
spinner_proto.text = clean_text(text)
|
||||
spinner_proto.cache = _cache
|
||||
spinner_proto.show_time = show_time
|
||||
message._enqueue("spinner", spinner_proto)
|
||||
|
||||
add_script_run_ctx(threading.Timer(DELAY_SECS, set_message)).start()
|
||||
|
||||
# Yield control back to the context.
|
||||
yield
|
||||
finally:
|
||||
if display_message_lock:
|
||||
with display_message_lock:
|
||||
display_message = False
|
||||
if "chat_message" in set(message._active_dg._ancestor_block_types):
|
||||
# Temporary stale element fix:
|
||||
# For chat messages, we are resetting the spinner placeholder to an
|
||||
# empty container instead of an empty placeholder (st.empty) to have
|
||||
# it removed from the delta path. Empty containers are ignored in the
|
||||
# frontend since they are configured with allow_empty=False. This
|
||||
# prevents issues with stale elements caused by the spinner being
|
||||
# rendered only in some situations (e.g. for caching).
|
||||
message.container()
|
||||
else:
|
||||
message.empty()
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.proto.Text_pb2 import Text as TextProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
class TextMixin:
|
||||
@gather_metrics("text")
|
||||
def text(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only arguments:
|
||||
help: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
r"""Write text without Markdown or HTML parsing.
|
||||
|
||||
For monospace text, use |st.code|_.
|
||||
|
||||
.. |st.code| replace:: ``st.code``
|
||||
.. _st.code: https://docs.streamlit.io/develop/api-reference/text/st.code
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The string to display.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the text. If this is ``None``
|
||||
(default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.text("This is text\n[and more text](that's not a Markdown link).")
|
||||
|
||||
.. output ::
|
||||
https://doc-text.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
text_proto = TextProto()
|
||||
text_proto.body = clean_text(body)
|
||||
if help:
|
||||
text_proto.help = help
|
||||
return self.dg._enqueue("text", text_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Toast_pb2 import Toast as ToastProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.string_util import clean_text, validate_icon_or_emoji
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.type_util import SupportsStr
|
||||
|
||||
|
||||
def validate_text(toast_text: SupportsStr) -> SupportsStr:
|
||||
if str(toast_text) == "":
|
||||
raise StreamlitAPIException(
|
||||
"Toast body cannot be blank - please provide a message."
|
||||
)
|
||||
else:
|
||||
return toast_text
|
||||
|
||||
|
||||
class ToastMixin:
|
||||
@gather_metrics("toast")
|
||||
def toast(
|
||||
self,
|
||||
body: SupportsStr,
|
||||
*, # keyword-only args:
|
||||
icon: str | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Display a short message, known as a notification "toast".
|
||||
The toast appears in the app's bottom-right corner and disappears after four seconds.
|
||||
|
||||
.. warning::
|
||||
``st.toast`` is not compatible with Streamlit's `caching \
|
||||
<https://docs.streamlit.io/develop/concepts/architecture/caching>`_ and
|
||||
cannot be called within a cached function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
body : str
|
||||
The string to display as GitHub-flavored Markdown. Syntax
|
||||
information can be found at: https://github.github.com/gfm.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
icon : str, None
|
||||
An optional emoji or icon to display next to the alert. If ``icon``
|
||||
is ``None`` (default), no icon is displayed. If ``icon`` is a
|
||||
string, the following options are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``icon="🚨"``
|
||||
or ``icon="🔥"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> st.toast('Your edited image was saved!', icon='😍')
|
||||
"""
|
||||
toast_proto = ToastProto()
|
||||
toast_proto.body = clean_text(validate_text(body))
|
||||
toast_proto.icon = validate_icon_or_emoji(icon)
|
||||
return self.dg._enqueue("toast", toast_proto)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
1985
myenv/lib/python3.11/site-packages/streamlit/elements/vega_charts.py
Normal file
1985
myenv/lib/python3.11/site-packages/streamlit/elements/vega_charts.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
@@ -0,0 +1,311 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.deprecation_util import (
|
||||
make_deprecated_name_warning,
|
||||
show_deprecation_warning,
|
||||
)
|
||||
from streamlit.elements.lib.file_uploader_utils import enforce_filename_restriction
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.elements.widgets.file_uploader import _get_upload_files
|
||||
from streamlit.proto.AudioInput_pb2 import AudioInput as AudioInputProto
|
||||
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
|
||||
from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
SomeUploadedAudioFile: TypeAlias = Union[UploadedFile, DeletedFile, None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AudioInputSerde:
|
||||
def serialize(
|
||||
self,
|
||||
audio_file: SomeUploadedAudioFile,
|
||||
) -> FileUploaderStateProto:
|
||||
state_proto = FileUploaderStateProto()
|
||||
|
||||
if audio_file is None or isinstance(audio_file, DeletedFile):
|
||||
return state_proto
|
||||
|
||||
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
|
||||
file_info.file_id = audio_file.file_id
|
||||
file_info.name = audio_file.name
|
||||
file_info.size = audio_file.size
|
||||
file_info.file_urls.CopyFrom(audio_file._file_urls)
|
||||
|
||||
return state_proto
|
||||
|
||||
def deserialize(
|
||||
self, ui_value: FileUploaderStateProto | None, widget_id: str
|
||||
) -> SomeUploadedAudioFile:
|
||||
upload_files = _get_upload_files(ui_value)
|
||||
if len(upload_files) == 0:
|
||||
return_value = None
|
||||
else:
|
||||
return_value = upload_files[0]
|
||||
if return_value is not None and not isinstance(return_value, DeletedFile):
|
||||
enforce_filename_restriction(return_value.name, [".wav"])
|
||||
return return_value
|
||||
|
||||
|
||||
class AudioInputMixin:
|
||||
@gather_metrics("audio_input")
|
||||
def audio_input(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> UploadedFile | None:
|
||||
r"""Display a widget that returns an audio recording from the user's microphone.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this widget is used for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this audio input's value
|
||||
changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the audio input if set to
|
||||
``True``. Default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None or UploadedFile
|
||||
The ``UploadedFile`` class is a subclass of ``BytesIO``, and
|
||||
therefore is "file-like". This means you can pass an instance of it
|
||||
anywhere a file is expected. The MIME type for the audio data is
|
||||
``audio/wav``.
|
||||
|
||||
.. Note::
|
||||
The resulting ``UploadedFile`` is subject to the size
|
||||
limitation configured in ``server.maxUploadSize``. If you
|
||||
expect large sound files, update the configuration option
|
||||
appropriately.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> audio_value = st.audio_input("Record a voice message")
|
||||
>>>
|
||||
>>> if audio_value:
|
||||
... st.audio(audio_value)
|
||||
|
||||
.. output::
|
||||
https://doc-audio-input.streamlit.app/
|
||||
height: 260px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._audio_input(
|
||||
label=label,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
@gather_metrics("experimental_audio_input")
|
||||
def experimental_audio_input(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> UploadedFile | None:
|
||||
"""Deprecated alias for st.audio_input.
|
||||
See the docstring for the widget's new name.
|
||||
"""
|
||||
|
||||
show_deprecation_warning(
|
||||
make_deprecated_name_warning(
|
||||
"experimental_audio_input",
|
||||
"audio_input",
|
||||
"2025-01-01",
|
||||
)
|
||||
)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
return self._audio_input(
|
||||
label=label,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _audio_input(
|
||||
self,
|
||||
label: str,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> UploadedFile | None:
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"audio_input",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
label=label,
|
||||
help=help,
|
||||
)
|
||||
|
||||
audio_input_proto = AudioInputProto()
|
||||
audio_input_proto.id = element_id
|
||||
audio_input_proto.label = label
|
||||
audio_input_proto.form_id = current_form_id(self.dg)
|
||||
audio_input_proto.disabled = disabled
|
||||
audio_input_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
if label and help is not None:
|
||||
audio_input_proto.help = dedent(help)
|
||||
|
||||
serde = AudioInputSerde()
|
||||
|
||||
audio_input_state = register_widget(
|
||||
audio_input_proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="file_uploader_state_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("audio_input", audio_input_proto)
|
||||
|
||||
if isinstance(audio_input_state.value, DeletedFile):
|
||||
return None
|
||||
return audio_input_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,263 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Union, cast
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.elements.lib.file_uploader_utils import enforce_filename_restriction
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.elements.widgets.file_uploader import _get_upload_files
|
||||
from streamlit.proto.CameraInput_pb2 import CameraInput as CameraInputProto
|
||||
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
|
||||
from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
SomeUploadedSnapshotFile: TypeAlias = Union[UploadedFile, DeletedFile, None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraInputSerde:
|
||||
def serialize(
|
||||
self,
|
||||
snapshot: SomeUploadedSnapshotFile,
|
||||
) -> FileUploaderStateProto:
|
||||
state_proto = FileUploaderStateProto()
|
||||
|
||||
if snapshot is None or isinstance(snapshot, DeletedFile):
|
||||
return state_proto
|
||||
|
||||
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
|
||||
file_info.file_id = snapshot.file_id
|
||||
file_info.name = snapshot.name
|
||||
file_info.size = snapshot.size
|
||||
file_info.file_urls.CopyFrom(snapshot._file_urls)
|
||||
|
||||
return state_proto
|
||||
|
||||
def deserialize(
|
||||
self, ui_value: FileUploaderStateProto | None, widget_id: str
|
||||
) -> SomeUploadedSnapshotFile:
|
||||
upload_files = _get_upload_files(ui_value)
|
||||
if len(upload_files) == 0:
|
||||
return_value = None
|
||||
else:
|
||||
return_value = upload_files[0]
|
||||
if return_value is not None and not isinstance(return_value, DeletedFile):
|
||||
enforce_filename_restriction(return_value.name, [".jpg"])
|
||||
return return_value
|
||||
|
||||
|
||||
class CameraInputMixin:
|
||||
@gather_metrics("camera_input")
|
||||
def camera_input(
|
||||
self,
|
||||
label: str,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> UploadedFile | None:
|
||||
r"""Display a widget that returns pictures from the user's webcam.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this widget is used for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this camera_input's value
|
||||
changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the camera input if set to
|
||||
``True``. Default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None or UploadedFile
|
||||
The UploadedFile class is a subclass of BytesIO, and therefore is
|
||||
"file-like". This means you can pass an instance of it anywhere a
|
||||
file is expected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> enable = st.checkbox("Enable camera")
|
||||
>>> picture = st.camera_input("Take a picture", disabled=not enable)
|
||||
>>>
|
||||
>>> if picture:
|
||||
... st.image(picture)
|
||||
|
||||
.. output::
|
||||
https://doc-camera-input.streamlit.app/
|
||||
height: 600px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._camera_input(
|
||||
label=label,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _camera_input(
|
||||
self,
|
||||
label: str,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> UploadedFile | None:
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"camera_input",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
label=label,
|
||||
help=help,
|
||||
)
|
||||
|
||||
camera_input_proto = CameraInputProto()
|
||||
camera_input_proto.id = element_id
|
||||
camera_input_proto.label = label
|
||||
camera_input_proto.form_id = current_form_id(self.dg)
|
||||
camera_input_proto.disabled = disabled
|
||||
camera_input_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
if help is not None:
|
||||
camera_input_proto.help = dedent(help)
|
||||
|
||||
serde = CameraInputSerde()
|
||||
|
||||
camera_input_state = register_widget(
|
||||
camera_input_proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="file_uploader_state_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("camera_input", camera_input_proto)
|
||||
|
||||
if isinstance(camera_input_state.value, DeletedFile):
|
||||
return None
|
||||
return camera_input_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,647 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator, MutableMapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from streamlit import config, runtime
|
||||
from streamlit.delta_generator_singletons import get_dg_singleton_instance
|
||||
from streamlit.elements.lib.file_uploader_utils import (
|
||||
enforce_filename_restriction,
|
||||
normalize_upload_file_type,
|
||||
)
|
||||
from streamlit.elements.lib.form_utils import is_in_form
|
||||
from streamlit.elements.lib.image_utils import AtomicImage, WidthBehavior, image_to_url
|
||||
from streamlit.elements.lib.policies import check_widget_policies
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
compute_and_register_element_id,
|
||||
get_chat_input_accept_file_proto_value,
|
||||
save_for_app_testing,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Block_pb2 import Block as BlockProto
|
||||
from streamlit.proto.ChatInput_pb2 import ChatInput as ChatInputProto
|
||||
from streamlit.proto.Common_pb2 import ChatInputValue as ChatInputValueProto
|
||||
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
|
||||
from streamlit.proto.RootContainer_pb2 import RootContainer
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
|
||||
from streamlit.string_util import is_emoji, validate_material_icon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatInputValue(MutableMapping[str, Any]):
|
||||
text: str
|
||||
files: list[UploadedFile]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(vars(self))
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(vars(self))
|
||||
|
||||
def __getitem__(self, item: str) -> str | list[UploadedFile]:
|
||||
try:
|
||||
return getattr(self, item) # type: ignore[no-any-return]
|
||||
except AttributeError:
|
||||
raise KeyError(f"Invalid key: {item}") from None
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
setattr(self, key, value)
|
||||
|
||||
def __delitem__(self, key: str) -> None:
|
||||
try:
|
||||
delattr(self, key)
|
||||
except AttributeError:
|
||||
raise KeyError(f"Invalid key: {key}") from None
|
||||
|
||||
def to_dict(self) -> dict[str, str | list[UploadedFile]]:
|
||||
return vars(self)
|
||||
|
||||
|
||||
class PresetNames(str, Enum):
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
AI = "ai" # Equivalent to assistant
|
||||
HUMAN = "human" # Equivalent to user
|
||||
|
||||
|
||||
def _process_avatar_input(
|
||||
avatar: str | AtomicImage | None, delta_path: str
|
||||
) -> tuple[BlockProto.ChatMessage.AvatarType.ValueType, str]:
|
||||
"""Detects the avatar type and prepares the avatar data for the frontend.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
avatar :
|
||||
The avatar that was provided by the user.
|
||||
delta_path : str
|
||||
The delta path is used as media ID when a local image is served via the media
|
||||
file manager.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple[AvatarType, str]
|
||||
The detected avatar type and the prepared avatar data.
|
||||
"""
|
||||
AvatarType = BlockProto.ChatMessage.AvatarType
|
||||
|
||||
if avatar is None:
|
||||
return AvatarType.ICON, ""
|
||||
elif isinstance(avatar, str) and avatar in {item.value for item in PresetNames}:
|
||||
# On the frontend, we only support "assistant" and "user" for the avatar.
|
||||
return (
|
||||
AvatarType.ICON,
|
||||
(
|
||||
"assistant"
|
||||
if avatar in [PresetNames.AI, PresetNames.ASSISTANT]
|
||||
else "user"
|
||||
),
|
||||
)
|
||||
elif isinstance(avatar, str) and is_emoji(avatar):
|
||||
return AvatarType.EMOJI, avatar
|
||||
|
||||
elif isinstance(avatar, str) and avatar.startswith(":material"):
|
||||
return AvatarType.ICON, validate_material_icon(avatar)
|
||||
else:
|
||||
try:
|
||||
return AvatarType.IMAGE, image_to_url(
|
||||
avatar,
|
||||
width=WidthBehavior.ORIGINAL,
|
||||
clamp=False,
|
||||
channels="RGB",
|
||||
output_format="auto",
|
||||
image_id=delta_path,
|
||||
)
|
||||
except Exception as ex:
|
||||
raise StreamlitAPIException(
|
||||
"Failed to load the provided avatar value as an image."
|
||||
) from ex
|
||||
|
||||
|
||||
def _pop_upload_files(
|
||||
files_value: FileUploaderStateProto | None,
|
||||
) -> list[UploadedFile]:
|
||||
if files_value is None:
|
||||
return []
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is None:
|
||||
return []
|
||||
|
||||
uploaded_file_info = files_value.uploaded_file_info
|
||||
if len(uploaded_file_info) == 0:
|
||||
return []
|
||||
|
||||
file_recs_list = ctx.uploaded_file_mgr.get_files(
|
||||
session_id=ctx.session_id,
|
||||
file_ids=[f.file_id for f in uploaded_file_info],
|
||||
)
|
||||
|
||||
file_recs = {f.file_id: f for f in file_recs_list}
|
||||
|
||||
collected_files: list[UploadedFile] = []
|
||||
|
||||
for f in uploaded_file_info:
|
||||
maybe_file_rec = file_recs.get(f.file_id)
|
||||
if maybe_file_rec is not None:
|
||||
uploaded_file = UploadedFile(maybe_file_rec, f.file_urls)
|
||||
collected_files.append(uploaded_file)
|
||||
|
||||
if hasattr(ctx.uploaded_file_mgr, "remove_file"):
|
||||
ctx.uploaded_file_mgr.remove_file(
|
||||
session_id=ctx.session_id,
|
||||
file_id=f.file_id,
|
||||
)
|
||||
|
||||
return collected_files
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChatInputSerde:
|
||||
accept_files: bool = False
|
||||
allowed_types: Sequence[str] | None = None
|
||||
|
||||
def deserialize(
|
||||
self,
|
||||
ui_value: ChatInputValueProto | None,
|
||||
widget_id: str = "",
|
||||
) -> str | ChatInputValue | None:
|
||||
if ui_value is None or not ui_value.HasField("data"):
|
||||
return None
|
||||
if not self.accept_files:
|
||||
return ui_value.data
|
||||
else:
|
||||
uploaded_files = _pop_upload_files(ui_value.file_uploader_state)
|
||||
for file in uploaded_files:
|
||||
if self.allowed_types and not isinstance(file, DeletedFile):
|
||||
enforce_filename_restriction(file.name, self.allowed_types)
|
||||
|
||||
return ChatInputValue(
|
||||
text=ui_value.data,
|
||||
files=uploaded_files,
|
||||
)
|
||||
|
||||
def serialize(self, v: str | None) -> ChatInputValueProto:
|
||||
return ChatInputValueProto(data=v)
|
||||
|
||||
|
||||
class ChatMixin:
|
||||
@gather_metrics("chat_message")
|
||||
def chat_message(
|
||||
self,
|
||||
name: Literal["user", "assistant", "ai", "human"] | str,
|
||||
*,
|
||||
avatar: Literal["user", "assistant"] | str | AtomicImage | None = None,
|
||||
) -> DeltaGenerator:
|
||||
"""Insert a chat message container.
|
||||
|
||||
To add elements to the returned container, you can use ``with`` notation
|
||||
(preferred) or just call methods directly on the returned object. See the
|
||||
examples below.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : "user", "assistant", "ai", "human", or str
|
||||
The name of the message author. Can be "human"/"user" or
|
||||
"ai"/"assistant" to enable preset styling and avatars.
|
||||
|
||||
Currently, the name is not shown in the UI but is only set as an
|
||||
accessibility label. For accessibility reasons, you should not use
|
||||
an empty string.
|
||||
|
||||
avatar : Anything supported by st.image (except list), str, or None
|
||||
The avatar shown next to the message.
|
||||
|
||||
If ``avatar`` is ``None`` (default), the icon will be determined
|
||||
from ``name`` as follows:
|
||||
|
||||
- If ``name`` is ``"user"`` or ``"human"``, the message will have a
|
||||
default user icon.
|
||||
|
||||
- If ``name`` is ``"ai"`` or ``"assistant"``, the message will have
|
||||
a default bot icon.
|
||||
|
||||
- For all other values of ``name``, the message will show the first
|
||||
letter of the name.
|
||||
|
||||
In addition to the types supported by |st.image|_ (except list),
|
||||
the following strings are valid:
|
||||
|
||||
- A single-character emoji. For example, you can set ``avatar="🧑💻"``
|
||||
or ``avatar="🦖"``. Emoji short codes are not supported.
|
||||
|
||||
- An icon from the Material Symbols library (rounded style) in the
|
||||
format ``":material/icon_name:"`` where "icon_name" is the name
|
||||
of the icon in snake case.
|
||||
|
||||
For example, ``icon=":material/thumb_up:"`` will display the
|
||||
Thumb Up icon. Find additional icons in the `Material Symbols \
|
||||
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
|
||||
font library.
|
||||
|
||||
.. |st.image| replace:: ``st.image``
|
||||
.. _st.image: https://docs.streamlit.io/develop/api-reference/media/st.image
|
||||
|
||||
Returns
|
||||
-------
|
||||
Container
|
||||
A single container that can hold multiple elements.
|
||||
|
||||
Examples
|
||||
--------
|
||||
You can use ``with`` notation to insert any element into an expander
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> with st.chat_message("user"):
|
||||
... st.write("Hello 👋")
|
||||
... st.line_chart(np.random.randn(30, 3))
|
||||
|
||||
.. output ::
|
||||
https://doc-chat-message-user.streamlit.app/
|
||||
height: 450px
|
||||
|
||||
Or you can just call methods directly in the returned objects:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import numpy as np
|
||||
>>>
|
||||
>>> message = st.chat_message("assistant")
|
||||
>>> message.write("Hello human")
|
||||
>>> message.bar_chart(np.random.randn(30, 3))
|
||||
|
||||
.. output ::
|
||||
https://doc-chat-message-user1.streamlit.app/
|
||||
height: 450px
|
||||
|
||||
"""
|
||||
if name is None:
|
||||
raise StreamlitAPIException(
|
||||
"The author name is required for a chat message, please set it via the parameter `name`."
|
||||
)
|
||||
|
||||
if avatar is None and (
|
||||
name.lower() in {item.value for item in PresetNames} or is_emoji(name)
|
||||
):
|
||||
# For selected labels, we are mapping the label to an avatar
|
||||
avatar = name.lower()
|
||||
avatar_type, converted_avatar = _process_avatar_input(
|
||||
avatar, self.dg._get_delta_path_str()
|
||||
)
|
||||
|
||||
message_container_proto = BlockProto.ChatMessage()
|
||||
message_container_proto.name = name
|
||||
message_container_proto.avatar = converted_avatar
|
||||
message_container_proto.avatar_type = avatar_type
|
||||
block_proto = BlockProto()
|
||||
block_proto.allow_empty = True
|
||||
block_proto.chat_message.CopyFrom(message_container_proto)
|
||||
|
||||
return self.dg._block(block_proto=block_proto)
|
||||
|
||||
@overload
|
||||
def chat_input(
|
||||
self,
|
||||
placeholder: str = "Your message",
|
||||
*,
|
||||
key: Key | None = None,
|
||||
max_chars: int | None = None,
|
||||
accept_file: Literal[False] = False,
|
||||
file_type: str | Sequence[str] | None = None,
|
||||
disabled: bool = False,
|
||||
on_submit: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
) -> str | None: ...
|
||||
|
||||
@overload
|
||||
def chat_input(
|
||||
self,
|
||||
placeholder: str = "Your message",
|
||||
*,
|
||||
key: Key | None = None,
|
||||
max_chars: int | None = None,
|
||||
accept_file: Literal[True, "multiple"],
|
||||
file_type: str | Sequence[str] | None = None,
|
||||
disabled: bool = False,
|
||||
on_submit: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
) -> ChatInputValue | None: ...
|
||||
|
||||
@gather_metrics("chat_input")
|
||||
def chat_input(
|
||||
self,
|
||||
placeholder: str = "Your message",
|
||||
*,
|
||||
key: Key | None = None,
|
||||
max_chars: int | None = None,
|
||||
accept_file: bool | Literal["multiple"] = False,
|
||||
file_type: str | Sequence[str] | None = None,
|
||||
disabled: bool = False,
|
||||
on_submit: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
) -> str | ChatInputValue | None:
|
||||
"""Display a chat input widget.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
placeholder : str
|
||||
A placeholder text shown when the chat input is empty. This
|
||||
defaults to ``"Your message"``. For accessibility reasons, you
|
||||
should not use an empty string.
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget based on
|
||||
its content. No two widgets may have the same key.
|
||||
|
||||
max_chars : int or None
|
||||
The maximum number of characters that can be entered. If this is
|
||||
``None`` (default), there will be no maximum.
|
||||
|
||||
accept_file : bool or str
|
||||
Whether the chat input should accept files. This can be one of the
|
||||
following values:
|
||||
|
||||
- ``False`` (default): No files are accepted and the user can only
|
||||
submit a message.
|
||||
- ``True``: The user can add a single file to their submission.
|
||||
- ``"multiple"``: The user can add multiple files to their
|
||||
submission.
|
||||
|
||||
When the widget is configured to accept files, the accepted file
|
||||
types can be configured with the ``file_type`` parameter.
|
||||
|
||||
By default, uploaded files are limited to 200 MB each. You can
|
||||
configure this using the ``server.maxUploadSize`` config option.
|
||||
For more information on how to set config options, see
|
||||
|config.toml|_.
|
||||
|
||||
.. |config.toml| replace:: ``config.toml``
|
||||
.. _config.toml: https://docs.streamlit.io/develop/api-reference/configuration/config.toml
|
||||
|
||||
file_type : str, Sequence[str], or None
|
||||
The allowed file extension(s) for uploaded files. This can be one
|
||||
of the following types:
|
||||
|
||||
- ``None`` (default): All file extensions are allowed.
|
||||
- A string: A single file extension is allowed. For example, to
|
||||
only accept CSV files, use ``"csv"``.
|
||||
- A sequence of strings: Multiple file extensions are allowed. For
|
||||
example, to only accept JPG/JPEG and PNG files, use
|
||||
``["jpg", "jpeg", "png"]``.
|
||||
|
||||
disabled : bool
|
||||
Whether the chat input should be disabled. This defaults to
|
||||
``False``.
|
||||
|
||||
on_submit : callable
|
||||
An optional callback invoked when the chat input's value is submitted.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None, str, or dict-like
|
||||
The user's submission. This is one of the following types:
|
||||
|
||||
- ``None``: If the user didn't submit a message or file in the last
|
||||
rerun, the widget returns ``None``.
|
||||
- A string: When the widget is not configured to accept files and
|
||||
the user submitted a message in the last rerun, the widget
|
||||
returns the user's message as a string.
|
||||
- A dict-like object: When the widget is configured to accept files
|
||||
and the user submitted a message and/or file(s) in the last
|
||||
rerun, the widget returns a dict-like object with two attributes,
|
||||
``text`` and ``files``.
|
||||
|
||||
When the widget is configured to accept files and the user submits
|
||||
something in the last rerun, you can access the user's submission
|
||||
with key or attribute notation from the dict-like object. This is
|
||||
shown in Example 3 below.
|
||||
|
||||
The ``text`` attribute holds a string, which is the user's message.
|
||||
This is an empty string if the user only submitted one or more
|
||||
files.
|
||||
|
||||
The ``files`` attribute holds a list of UploadedFile objects.
|
||||
The list is empty if the user only submitted a message. Unlike
|
||||
``st.file_uploader``, this attribute always returns a list, even
|
||||
when the widget is configured to accept only one file at a time.
|
||||
|
||||
The UploadedFile class is a subclass of BytesIO, and therefore is
|
||||
"file-like". This means you can pass an instance of it anywhere a
|
||||
file is expected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
**Example 1: Pin the the chat input widget to the bottom of your app**
|
||||
|
||||
When ``st.chat_input`` is used in the main body of an app, it will be
|
||||
pinned to the bottom of the page.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> prompt = st.chat_input("Say something")
|
||||
>>> if prompt:
|
||||
... st.write(f"User has sent the following prompt: {prompt}")
|
||||
|
||||
.. output ::
|
||||
https://doc-chat-input.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
**Example 2: Use the chat input widget inline**
|
||||
|
||||
The chat input can also be used inline by nesting it inside any layout
|
||||
container (container, columns, tabs, sidebar, etc) or fragment. Create
|
||||
chat interfaces embedded next to other content, or have multiple
|
||||
chatbots!
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> with st.sidebar:
|
||||
>>> messages = st.container(height=300)
|
||||
>>> if prompt := st.chat_input("Say something"):
|
||||
>>> messages.chat_message("user").write(prompt)
|
||||
>>> messages.chat_message("assistant").write(f"Echo: {prompt}")
|
||||
|
||||
.. output ::
|
||||
https://doc-chat-input-inline.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
**Example 3: Let users upload files**
|
||||
|
||||
When you configure your chat input widget to allow file attachments, it
|
||||
will return a dict-like object when the user sends a submission. You
|
||||
can access the user's message through the ``text`` attribute of this
|
||||
dictionary. You can access a list of the user's submitted file(s)
|
||||
through the ``files`` attribute. Similar to ``st.session_state``, you
|
||||
can use key or attribute notation.
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> prompt = st.chat_input(
|
||||
>>> "Say something and/or attach an image",
|
||||
>>> accept_file=True,
|
||||
>>> file_type=["jpg", "jpeg", "png"],
|
||||
>>> )
|
||||
>>> if prompt and prompt.text:
|
||||
>>> st.markdown(prompt.text)
|
||||
>>> if prompt and prompt["files"]:
|
||||
>>> st.image(prompt["files"][0])
|
||||
|
||||
.. output ::
|
||||
https://doc-chat-input-file-uploader.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
# We default to an empty string here and disallow user choice intentionally
|
||||
default = ""
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_submit,
|
||||
default_value=default,
|
||||
writes_allowed=False,
|
||||
)
|
||||
|
||||
if accept_file not in {True, False, "multiple"}:
|
||||
raise StreamlitAPIException(
|
||||
"The `accept_file` parameter must be a boolean or 'multiple'."
|
||||
)
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"chat_input",
|
||||
user_key=key,
|
||||
# chat_input is not allowed to be used in a form.
|
||||
form_id=None,
|
||||
placeholder=placeholder,
|
||||
max_chars=max_chars,
|
||||
accept_file=accept_file,
|
||||
file_type=file_type,
|
||||
)
|
||||
|
||||
if file_type:
|
||||
file_type = normalize_upload_file_type(file_type)
|
||||
|
||||
# It doesn't make sense to create a chat input inside a form.
|
||||
# We throw an error to warn the user about this.
|
||||
# We omit this check for scripts running outside streamlit, because
|
||||
# they will have no script_run_ctx.
|
||||
if runtime.exists():
|
||||
if is_in_form(self.dg):
|
||||
raise StreamlitAPIException(
|
||||
"`st.chat_input()` can't be used in a `st.form()`."
|
||||
)
|
||||
|
||||
# Determine the position of the chat input:
|
||||
# Use bottom position if chat input is within the main container
|
||||
# either directly or within a vertical container. If it has any
|
||||
# other container types as parents, we use inline position.
|
||||
ancestor_block_types = set(self.dg._active_dg._ancestor_block_types)
|
||||
if (
|
||||
self.dg._active_dg._root_container == RootContainer.MAIN
|
||||
and not ancestor_block_types
|
||||
):
|
||||
position = "bottom"
|
||||
else:
|
||||
position = "inline"
|
||||
|
||||
chat_input_proto = ChatInputProto()
|
||||
chat_input_proto.id = element_id
|
||||
chat_input_proto.placeholder = str(placeholder)
|
||||
|
||||
if max_chars is not None:
|
||||
chat_input_proto.max_chars = max_chars
|
||||
|
||||
chat_input_proto.default = default
|
||||
|
||||
chat_input_proto.accept_file = get_chat_input_accept_file_proto_value(
|
||||
accept_file
|
||||
)
|
||||
|
||||
chat_input_proto.file_type[:] = file_type if file_type is not None else []
|
||||
chat_input_proto.max_upload_size_mb = config.get_option("server.maxUploadSize")
|
||||
|
||||
serde = ChatInputSerde(
|
||||
accept_files=bool(accept_file),
|
||||
allowed_types=file_type,
|
||||
)
|
||||
widget_state = register_widget( # type: ignore[misc]
|
||||
chat_input_proto.id,
|
||||
on_change_handler=on_submit,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="chat_input_value",
|
||||
)
|
||||
|
||||
chat_input_proto.disabled = disabled
|
||||
if widget_state.value_changed and widget_state.value is not None:
|
||||
chat_input_proto.value = widget_state.value
|
||||
chat_input_proto.set_value = True
|
||||
|
||||
if ctx:
|
||||
save_for_app_testing(ctx, element_id, widget_state.value)
|
||||
if position == "bottom":
|
||||
# We need to enqueue the chat input into the bottom container
|
||||
# instead of the currently active dg.
|
||||
get_dg_singleton_instance().bottom_dg._enqueue(
|
||||
"chat_input", chat_input_proto
|
||||
)
|
||||
else:
|
||||
self.dg._enqueue("chat_input", chat_input_proto)
|
||||
|
||||
return widget_state.value if not widget_state.value_changed else None
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,352 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.proto.Checkbox_pb2 import Checkbox as CheckboxProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckboxSerde:
|
||||
value: bool
|
||||
|
||||
def serialize(self, v: bool) -> bool:
|
||||
return bool(v)
|
||||
|
||||
def deserialize(self, ui_value: bool | None, widget_id: str = "") -> bool:
|
||||
return bool(ui_value if ui_value is not None else self.value)
|
||||
|
||||
|
||||
class CheckboxMixin:
|
||||
@gather_metrics("checkbox")
|
||||
def checkbox(
|
||||
self,
|
||||
label: str,
|
||||
value: bool = False,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> bool:
|
||||
r"""Display a checkbox widget.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this checkbox is for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
value : bool
|
||||
Preselect the checkbox when it first renders. This will be
|
||||
cast to bool internally.
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this checkbox's value changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the checkbox if set to ``True``.
|
||||
The default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether or not the checkbox is checked.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> agree = st.checkbox("I agree")
|
||||
>>>
|
||||
>>> if agree:
|
||||
... st.write("Great!")
|
||||
|
||||
.. output::
|
||||
https://doc-checkbox.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._checkbox(
|
||||
label=label,
|
||||
value=value,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
type=CheckboxProto.StyleType.DEFAULT,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
@gather_metrics("toggle")
|
||||
def toggle(
|
||||
self,
|
||||
label: str,
|
||||
value: bool = False,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> bool:
|
||||
r"""Display a toggle widget.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this toggle is for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
value : bool
|
||||
Preselect the toggle when it first renders. This will be
|
||||
cast to bool internally.
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this toggle's value changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the toggle if set to ``True``.
|
||||
The default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether or not the toggle is checked.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> on = st.toggle("Activate feature")
|
||||
>>>
|
||||
>>> if on:
|
||||
... st.write("Feature activated!")
|
||||
|
||||
.. output::
|
||||
https://doc-toggle.streamlit.app/
|
||||
height: 220px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._checkbox(
|
||||
label=label,
|
||||
value=value,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
type=CheckboxProto.StyleType.TOGGLE,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _checkbox(
|
||||
self,
|
||||
label: str,
|
||||
value: bool = False,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
type: CheckboxProto.StyleType.ValueType = CheckboxProto.StyleType.DEFAULT,
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> bool:
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=None if value is False else value,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"toggle" if type == CheckboxProto.StyleType.TOGGLE else "checkbox",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
label=label,
|
||||
value=bool(value),
|
||||
help=help,
|
||||
)
|
||||
|
||||
checkbox_proto = CheckboxProto()
|
||||
checkbox_proto.id = element_id
|
||||
checkbox_proto.label = label
|
||||
checkbox_proto.default = bool(value)
|
||||
checkbox_proto.type = type
|
||||
checkbox_proto.form_id = current_form_id(self.dg)
|
||||
checkbox_proto.disabled = disabled
|
||||
checkbox_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
if help is not None:
|
||||
checkbox_proto.help = dedent(help)
|
||||
|
||||
serde = CheckboxSerde(value)
|
||||
|
||||
checkbox_state = register_widget(
|
||||
checkbox_proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="bool_value",
|
||||
)
|
||||
|
||||
if checkbox_state.value_changed:
|
||||
checkbox_proto.value = checkbox_state.value
|
||||
checkbox_proto.set_value = True
|
||||
|
||||
self.dg._enqueue("checkbox", checkbox_proto)
|
||||
return checkbox_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,265 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.ColorPicker_pb2 import ColorPicker as ColorPickerProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorPickerSerde:
|
||||
value: str
|
||||
|
||||
def serialize(self, v: str) -> str:
|
||||
return str(v)
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> str:
|
||||
return str(ui_value if ui_value is not None else self.value)
|
||||
|
||||
|
||||
class ColorPickerMixin:
|
||||
@gather_metrics("color_picker")
|
||||
def color_picker(
|
||||
self,
|
||||
label: str,
|
||||
value: str | None = None,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> str:
|
||||
r"""Display a color picker widget.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this input is for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
value : str
|
||||
The hex value of this widget when it first renders. If None,
|
||||
defaults to black.
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this color_picker's value
|
||||
changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the color picker if set to
|
||||
``True``. The default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The selected color as a hex string.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> color = st.color_picker("Pick A Color", "#00f900")
|
||||
>>> st.write("The current color is", color)
|
||||
|
||||
.. output::
|
||||
https://doc-color-picker.streamlit.app/
|
||||
height: 335px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._color_picker(
|
||||
label=label,
|
||||
value=value,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _color_picker(
|
||||
self,
|
||||
label: str,
|
||||
value: str | None = None,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> str:
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=value,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"color_picker",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
label=label,
|
||||
value=str(value),
|
||||
help=help,
|
||||
)
|
||||
|
||||
# set value default
|
||||
if value is None:
|
||||
value = "#000000"
|
||||
|
||||
# make sure the value is a string
|
||||
if not isinstance(value, str):
|
||||
raise StreamlitAPIException(
|
||||
"""
|
||||
Color Picker Value has invalid type: %s. Expects a hex string
|
||||
like '#00FFAA' or '#000'.
|
||||
"""
|
||||
% type(value).__name__
|
||||
)
|
||||
|
||||
# validate the value and expects a hex string
|
||||
match = re.match(r"^#(?:[0-9a-fA-F]{3}){1,2}$", value)
|
||||
|
||||
if not match:
|
||||
raise StreamlitAPIException(
|
||||
"""
|
||||
'%s' is not a valid hex code for colors. Valid ones are like
|
||||
'#00FFAA' or '#000'.
|
||||
"""
|
||||
% value
|
||||
)
|
||||
|
||||
color_picker_proto = ColorPickerProto()
|
||||
color_picker_proto.id = element_id
|
||||
color_picker_proto.label = label
|
||||
color_picker_proto.default = str(value)
|
||||
color_picker_proto.form_id = current_form_id(self.dg)
|
||||
color_picker_proto.disabled = disabled
|
||||
color_picker_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
if help is not None:
|
||||
color_picker_proto.help = dedent(help)
|
||||
|
||||
serde = ColorPickerSerde(value)
|
||||
|
||||
widget_state = register_widget(
|
||||
color_picker_proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="string_value",
|
||||
)
|
||||
|
||||
if widget_state.value_changed:
|
||||
color_picker_proto.value = widget_state.value
|
||||
color_picker_proto.set_value = True
|
||||
|
||||
self.dg._enqueue("color_picker", color_picker_proto)
|
||||
return widget_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,982 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from decimal import Decimal
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
Literal,
|
||||
TypedDict,
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
overload,
|
||||
)
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import dataframe_util
|
||||
from streamlit import logger as _logger
|
||||
from streamlit.elements.lib.column_config_utils import (
|
||||
INDEX_IDENTIFIER,
|
||||
ColumnConfigMapping,
|
||||
ColumnConfigMappingInput,
|
||||
ColumnDataKind,
|
||||
DataframeSchema,
|
||||
apply_data_specific_configs,
|
||||
determine_dataframe_schema,
|
||||
is_type_compatible,
|
||||
marshall_column_config,
|
||||
process_config_mapping,
|
||||
update_column_config,
|
||||
)
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.pandas_styler_utils import marshall_styler
|
||||
from streamlit.elements.lib.policies import check_widget_policies
|
||||
from streamlit.elements.lib.utils import Key, compute_and_register_element_id, to_key
|
||||
from streamlit.errors import StreamlitAPIException
|
||||
from streamlit.proto.Arrow_pb2 import Arrow as ArrowProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
from streamlit.type_util import is_type
|
||||
from streamlit.util import calc_md5
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Mapping
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
from pandas.io.formats.style import Styler
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
_LOGGER: Final = _logger.get_logger(__name__)
|
||||
|
||||
# All formats that support direct editing, meaning that these
|
||||
# formats will be returned with the same type when used with data_editor.
|
||||
EditableData = TypeVar(
|
||||
"EditableData",
|
||||
bound=Union[
|
||||
dataframe_util.DataFrameGenericAlias[Any], # covers DataFrame and Series
|
||||
tuple[Any],
|
||||
list[Any],
|
||||
set[Any],
|
||||
dict[str, Any],
|
||||
# TODO(lukasmasuch): Add support for np.ndarray
|
||||
# but it is not possible with np.ndarray.
|
||||
# NDArray[Any] works, but is only available in numpy>1.20.
|
||||
# TODO(lukasmasuch): Add support for pa.Table typing
|
||||
# pa.Table does not work since it is a C-based class resulting in Any
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# All data types supported by the data editor.
|
||||
DataTypes: TypeAlias = Union[
|
||||
"pd.DataFrame",
|
||||
"pd.Series",
|
||||
"pd.Index",
|
||||
"Styler",
|
||||
"pa.Table",
|
||||
"np.ndarray[Any, np.dtype[np.float64]]",
|
||||
tuple[Any],
|
||||
list[Any],
|
||||
set[Any],
|
||||
dict[str, Any],
|
||||
]
|
||||
|
||||
|
||||
class EditingState(TypedDict, total=False):
|
||||
"""
|
||||
A dictionary representing the current state of the data editor.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
edited_rows : Dict[int, Dict[str, str | int | float | bool | None]]
|
||||
An hierarchical mapping of edited cells based on:
|
||||
row position -> column name -> value.
|
||||
|
||||
added_rows : List[Dict[str, str | int | float | bool | None]]
|
||||
A list of added rows, where each row is a mapping from column name to
|
||||
the cell value.
|
||||
|
||||
deleted_rows : List[int]
|
||||
A list of deleted rows, where each row is the numerical position of
|
||||
the deleted row.
|
||||
"""
|
||||
|
||||
edited_rows: dict[int, dict[str, str | int | float | bool | None]]
|
||||
added_rows: list[dict[str, str | int | float | bool | None]]
|
||||
deleted_rows: list[int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DataEditorSerde:
|
||||
"""DataEditorSerde is used to serialize and deserialize the data editor state."""
|
||||
|
||||
def deserialize(self, ui_value: str | None, widget_id: str = "") -> EditingState:
|
||||
data_editor_state: EditingState = (
|
||||
{
|
||||
"edited_rows": {},
|
||||
"added_rows": [],
|
||||
"deleted_rows": [],
|
||||
}
|
||||
if ui_value is None
|
||||
else json.loads(ui_value)
|
||||
)
|
||||
|
||||
# Make sure that all editing state keys are present:
|
||||
if "edited_rows" not in data_editor_state:
|
||||
data_editor_state["edited_rows"] = {}
|
||||
|
||||
if "deleted_rows" not in data_editor_state:
|
||||
data_editor_state["deleted_rows"] = []
|
||||
|
||||
if "added_rows" not in data_editor_state:
|
||||
data_editor_state["added_rows"] = []
|
||||
|
||||
# Convert the keys (numerical row positions) to integers.
|
||||
# The keys are strings because they are serialized to JSON.
|
||||
data_editor_state["edited_rows"] = {
|
||||
int(k): v for k, v in data_editor_state["edited_rows"].items()
|
||||
}
|
||||
return data_editor_state
|
||||
|
||||
def serialize(self, editing_state: EditingState) -> str:
|
||||
return json.dumps(editing_state, default=str)
|
||||
|
||||
|
||||
def _parse_value(
|
||||
value: str | int | float | bool | None,
|
||||
column_data_kind: ColumnDataKind,
|
||||
) -> Any:
|
||||
"""Convert a value to the correct type.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : str | int | float | bool | None
|
||||
The value to convert.
|
||||
|
||||
column_data_kind : ColumnDataKind
|
||||
The determined data kind of the column. The column data kind refers to the
|
||||
shared data type of the values in the column (e.g. int, float, str).
|
||||
|
||||
Returns
|
||||
-------
|
||||
The converted value.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
import pandas as pd
|
||||
|
||||
try:
|
||||
if column_data_kind == ColumnDataKind.STRING:
|
||||
return str(value)
|
||||
|
||||
if column_data_kind == ColumnDataKind.INTEGER:
|
||||
return int(value)
|
||||
|
||||
if column_data_kind == ColumnDataKind.FLOAT:
|
||||
return float(value)
|
||||
|
||||
if column_data_kind == ColumnDataKind.BOOLEAN:
|
||||
return bool(value)
|
||||
|
||||
if column_data_kind == ColumnDataKind.DECIMAL:
|
||||
# Decimal theoretically can also be initialized via number values.
|
||||
# However, using number values here seems to cause issues with Arrow
|
||||
# serialization, once you try to render the returned dataframe.
|
||||
return Decimal(str(value))
|
||||
|
||||
if column_data_kind == ColumnDataKind.TIMEDELTA:
|
||||
return pd.Timedelta(value)
|
||||
|
||||
if column_data_kind in [
|
||||
ColumnDataKind.DATETIME,
|
||||
ColumnDataKind.DATE,
|
||||
ColumnDataKind.TIME,
|
||||
]:
|
||||
datetime_value = pd.Timestamp(value)
|
||||
|
||||
if datetime_value is pd.NaT:
|
||||
return None
|
||||
|
||||
if column_data_kind == ColumnDataKind.DATETIME:
|
||||
return datetime_value
|
||||
|
||||
if column_data_kind == ColumnDataKind.DATE:
|
||||
return datetime_value.date()
|
||||
|
||||
if column_data_kind == ColumnDataKind.TIME:
|
||||
return datetime_value.time()
|
||||
|
||||
except (ValueError, pd.errors.ParserError) as ex:
|
||||
_LOGGER.warning(
|
||||
"Failed to parse value %s as %s.",
|
||||
value,
|
||||
column_data_kind,
|
||||
exc_info=ex,
|
||||
)
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def _apply_cell_edits(
|
||||
df: pd.DataFrame,
|
||||
edited_rows: Mapping[int, Mapping[str, str | int | float | bool | None]],
|
||||
dataframe_schema: DataframeSchema,
|
||||
) -> None:
|
||||
"""Apply cell edits to the provided dataframe (inplace).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
The dataframe to apply the cell edits to.
|
||||
|
||||
edited_rows : Mapping[int, Mapping[str, str | int | float | bool | None]]
|
||||
A hierarchical mapping based on row position -> column name -> value
|
||||
|
||||
dataframe_schema: DataframeSchema
|
||||
The schema of the dataframe.
|
||||
"""
|
||||
for row_id, row_changes in edited_rows.items():
|
||||
row_pos = int(row_id)
|
||||
for col_name, value in row_changes.items():
|
||||
if col_name == INDEX_IDENTIFIER:
|
||||
# The edited cell is part of the index
|
||||
# TODO(lukasmasuch): To support multi-index in the future:
|
||||
# use a tuple of values here instead of a single value
|
||||
df.index.to_numpy()[row_pos] = _parse_value(
|
||||
value, dataframe_schema[INDEX_IDENTIFIER]
|
||||
)
|
||||
else:
|
||||
col_pos = df.columns.get_loc(col_name)
|
||||
df.iloc[row_pos, col_pos] = _parse_value(
|
||||
value, dataframe_schema[col_name]
|
||||
)
|
||||
|
||||
|
||||
def _apply_row_additions(
|
||||
df: pd.DataFrame,
|
||||
added_rows: list[dict[str, Any]],
|
||||
dataframe_schema: DataframeSchema,
|
||||
) -> None:
|
||||
"""Apply row additions to the provided dataframe (inplace).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
The dataframe to apply the row additions to.
|
||||
|
||||
added_rows : List[Dict[str, Any]]
|
||||
A list of row additions. Each row addition is a dictionary with the
|
||||
column position as key and the new cell value as value.
|
||||
|
||||
dataframe_schema: DataframeSchema
|
||||
The schema of the dataframe.
|
||||
"""
|
||||
|
||||
if not added_rows:
|
||||
return
|
||||
|
||||
import pandas as pd
|
||||
|
||||
# This is only used if the dataframe has a range index:
|
||||
# There seems to be a bug in older pandas versions with RangeIndex in
|
||||
# combination with loc. As a workaround, we manually track the values here:
|
||||
range_index_stop = None
|
||||
range_index_step = None
|
||||
if isinstance(df.index, pd.RangeIndex):
|
||||
range_index_stop = df.index.stop
|
||||
range_index_step = df.index.step
|
||||
|
||||
for added_row in added_rows:
|
||||
index_value = None
|
||||
new_row: list[Any] = [None for _ in range(df.shape[1])]
|
||||
for col_name in added_row.keys():
|
||||
value = added_row[col_name]
|
||||
if col_name == INDEX_IDENTIFIER:
|
||||
# TODO(lukasmasuch): To support multi-index in the future:
|
||||
# use a tuple of values here instead of a single value
|
||||
index_value = _parse_value(value, dataframe_schema[INDEX_IDENTIFIER])
|
||||
else:
|
||||
col_pos = df.columns.get_loc(col_name)
|
||||
new_row[col_pos] = _parse_value(value, dataframe_schema[col_name])
|
||||
# Append the new row to the dataframe
|
||||
if range_index_stop is not None:
|
||||
df.loc[range_index_stop, :] = new_row
|
||||
# Increment to the next range index value
|
||||
range_index_stop += range_index_step
|
||||
elif index_value is not None:
|
||||
# TODO(lukasmasuch): we are only adding rows that have a non-None index
|
||||
# value to prevent issues in the frontend component. Also, it just overwrites
|
||||
# the row in case the index value already exists in the dataframe.
|
||||
# In the future, it would be better to require users to provide unique
|
||||
# non-None values for the index with some kind of visual indications.
|
||||
df.loc[index_value, :] = new_row
|
||||
|
||||
|
||||
def _apply_row_deletions(df: pd.DataFrame, deleted_rows: list[int]) -> None:
|
||||
"""Apply row deletions to the provided dataframe (inplace).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
The dataframe to apply the row deletions to.
|
||||
|
||||
deleted_rows : List[int]
|
||||
A list of row numbers to delete.
|
||||
"""
|
||||
# Drop rows based in numeric row positions
|
||||
df.drop(df.index[deleted_rows], inplace=True) # noqa: PD002
|
||||
|
||||
|
||||
def _apply_dataframe_edits(
|
||||
df: pd.DataFrame,
|
||||
data_editor_state: EditingState,
|
||||
dataframe_schema: DataframeSchema,
|
||||
) -> None:
|
||||
"""Apply edits to the provided dataframe (inplace).
|
||||
|
||||
This includes cell edits, row additions and row deletions.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
The dataframe to apply the edits to.
|
||||
|
||||
data_editor_state : EditingState
|
||||
The editing state of the data editor component.
|
||||
|
||||
dataframe_schema: DataframeSchema
|
||||
The schema of the dataframe.
|
||||
"""
|
||||
if data_editor_state.get("edited_rows"):
|
||||
_apply_cell_edits(df, data_editor_state["edited_rows"], dataframe_schema)
|
||||
|
||||
if data_editor_state.get("deleted_rows"):
|
||||
_apply_row_deletions(df, data_editor_state["deleted_rows"])
|
||||
|
||||
if data_editor_state.get("added_rows"):
|
||||
# The addition of new rows needs to happen after the deletion to not have
|
||||
# unexpected side-effects, like https://github.com/streamlit/streamlit/issues/8854
|
||||
_apply_row_additions(df, data_editor_state["added_rows"], dataframe_schema)
|
||||
|
||||
|
||||
def _is_supported_index(df_index: pd.Index) -> bool:
|
||||
"""Check if the index is supported by the data editor component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df_index : pd.Index
|
||||
The index to check.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if the index is supported, False otherwise.
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
return (
|
||||
type(df_index)
|
||||
in [
|
||||
pd.RangeIndex,
|
||||
pd.Index,
|
||||
pd.DatetimeIndex,
|
||||
pd.CategoricalIndex,
|
||||
# Interval type isn't editable currently:
|
||||
# pd.IntervalIndex,
|
||||
# Period type isn't editable currently:
|
||||
# pd.PeriodIndex,
|
||||
]
|
||||
# We need to check these index types without importing, since they are
|
||||
# deprecated and planned to be removed soon.
|
||||
or is_type(df_index, "pandas.core.indexes.numeric.Int64Index")
|
||||
or is_type(df_index, "pandas.core.indexes.numeric.Float64Index")
|
||||
or is_type(df_index, "pandas.core.indexes.numeric.UInt64Index")
|
||||
)
|
||||
|
||||
|
||||
def _fix_column_headers(data_df: pd.DataFrame) -> None:
|
||||
"""Fix the column headers of the provided dataframe inplace to work
|
||||
correctly for data editing.
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
if isinstance(data_df.columns, pd.MultiIndex):
|
||||
# Flatten hierarchical column headers to a single level:
|
||||
data_df.columns = [
|
||||
"_".join(map(str, header)) for header in data_df.columns.to_flat_index()
|
||||
]
|
||||
elif pd.api.types.infer_dtype(data_df.columns) != "string":
|
||||
# If the column names are not all strings, we need to convert them to strings
|
||||
# to avoid issues with editing:
|
||||
data_df.rename(
|
||||
columns={column: str(column) for column in data_df.columns},
|
||||
inplace=True, # noqa: PD002
|
||||
)
|
||||
|
||||
|
||||
def _check_column_names(data_df: pd.DataFrame):
|
||||
"""Check if the column names in the provided dataframe are valid.
|
||||
|
||||
It's not allowed to have duplicate column names or column names that are
|
||||
named ``_index``. If the column names are not valid, a ``StreamlitAPIException``
|
||||
is raised.
|
||||
"""
|
||||
|
||||
if data_df.columns.empty:
|
||||
return
|
||||
|
||||
# Check if the column names are unique and raise an exception if not.
|
||||
# Add the names of the duplicated columns to the exception message.
|
||||
duplicated_columns = data_df.columns[data_df.columns.duplicated()]
|
||||
if len(duplicated_columns) > 0:
|
||||
raise StreamlitAPIException(
|
||||
f"All column names are required to be unique for usage with data editor. "
|
||||
f"The following column names are duplicated: {list(duplicated_columns)}. "
|
||||
f"Please rename the duplicated columns in the provided data."
|
||||
)
|
||||
|
||||
# Check if the column names are not named "_index" and raise an exception if so.
|
||||
if INDEX_IDENTIFIER in data_df.columns:
|
||||
raise StreamlitAPIException(
|
||||
f"The column name '{INDEX_IDENTIFIER}' is reserved for the index column "
|
||||
f"and can't be used for data columns. Please rename the column in the "
|
||||
f"provided data."
|
||||
)
|
||||
|
||||
|
||||
def _check_type_compatibilities(
|
||||
data_df: pd.DataFrame,
|
||||
columns_config: ColumnConfigMapping,
|
||||
dataframe_schema: DataframeSchema,
|
||||
):
|
||||
"""Check column type to data type compatibility.
|
||||
|
||||
Iterates the index and all columns of the dataframe to check if
|
||||
the configured column types are compatible with the underlying data types.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data_df : pd.DataFrame
|
||||
The dataframe to check the type compatibilities for.
|
||||
|
||||
columns_config : ColumnConfigMapping
|
||||
A mapping of column to column configurations.
|
||||
|
||||
dataframe_schema : DataframeSchema
|
||||
The schema of the dataframe.
|
||||
|
||||
Raises
|
||||
------
|
||||
StreamlitAPIException
|
||||
If a configured column type is editable and not compatible with the
|
||||
underlying data type.
|
||||
"""
|
||||
# TODO(lukasmasuch): Update this here to support multi-index in the future:
|
||||
indices = [(INDEX_IDENTIFIER, data_df.index)]
|
||||
|
||||
for column in indices + list(data_df.items()):
|
||||
column_name, _ = column
|
||||
column_data_kind = dataframe_schema[column_name]
|
||||
|
||||
# TODO(lukasmasuch): support column config via numerical index here?
|
||||
if column_name in columns_config:
|
||||
column_config = columns_config[column_name]
|
||||
if column_config.get("disabled") is True:
|
||||
# Disabled columns are not checked for compatibility.
|
||||
# This might change in the future.
|
||||
continue
|
||||
|
||||
type_config = column_config.get("type_config")
|
||||
|
||||
if type_config is None:
|
||||
continue
|
||||
|
||||
configured_column_type = type_config.get("type")
|
||||
|
||||
if configured_column_type is None:
|
||||
continue
|
||||
|
||||
if is_type_compatible(configured_column_type, column_data_kind) is False:
|
||||
raise StreamlitAPIException(
|
||||
f"The configured column type `{configured_column_type}` for column "
|
||||
f"`{column_name}` is not compatible for editing the underlying "
|
||||
f"data type `{column_data_kind}`.\n\nYou have following options to "
|
||||
f"fix this: 1) choose a compatible type 2) disable the column "
|
||||
f"3) convert the column into a compatible data type."
|
||||
)
|
||||
|
||||
|
||||
class DataEditorMixin:
|
||||
@overload
|
||||
def data_editor(
|
||||
self,
|
||||
data: EditableData,
|
||||
*,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
use_container_width: bool | None = None,
|
||||
hide_index: bool | None = None,
|
||||
column_order: Iterable[str] | None = None,
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
num_rows: Literal["fixed", "dynamic"] = "fixed",
|
||||
disabled: bool | Iterable[str] = False,
|
||||
key: Key | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
row_height: int | None = None,
|
||||
) -> EditableData:
|
||||
pass
|
||||
|
||||
@overload
|
||||
def data_editor(
|
||||
self,
|
||||
data: Any,
|
||||
*,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
use_container_width: bool | None = None,
|
||||
hide_index: bool | None = None,
|
||||
column_order: Iterable[str] | None = None,
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
num_rows: Literal["fixed", "dynamic"] = "fixed",
|
||||
disabled: bool | Iterable[str] = False,
|
||||
key: Key | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
row_height: int | None = None,
|
||||
) -> pd.DataFrame:
|
||||
pass
|
||||
|
||||
@gather_metrics("data_editor")
|
||||
def data_editor(
|
||||
self,
|
||||
data: DataTypes,
|
||||
*,
|
||||
width: int | None = None,
|
||||
height: int | None = None,
|
||||
use_container_width: bool | None = None,
|
||||
hide_index: bool | None = None,
|
||||
column_order: Iterable[str] | None = None,
|
||||
column_config: ColumnConfigMappingInput | None = None,
|
||||
num_rows: Literal["fixed", "dynamic"] = "fixed",
|
||||
disabled: bool | Iterable[str] = False,
|
||||
key: Key | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
row_height: int | None = None,
|
||||
) -> DataTypes:
|
||||
"""Display a data editor widget.
|
||||
|
||||
The data editor widget allows you to edit dataframes and many other data structures in a table-like UI.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : Anything supported by st.dataframe
|
||||
The data to edit in the data editor.
|
||||
|
||||
.. note::
|
||||
- Styles from ``pandas.Styler`` will only be applied to non-editable columns.
|
||||
- Text and number formatting from ``column_config`` always takes
|
||||
precedence over text and number formatting from ``pandas.Styler``.
|
||||
- Mixing data types within a column can make the column uneditable.
|
||||
- Additionally, the following data types are not yet supported for editing:
|
||||
``complex``, ``list``, ``tuple``, ``bytes``, ``bytearray``,
|
||||
``memoryview``, ``dict``, ``set``, ``frozenset``,
|
||||
``fractions.Fraction``, ``pandas.Interval``, and
|
||||
``pandas.Period``.
|
||||
- To prevent overflow in JavaScript, columns containing
|
||||
``datetime.timedelta`` and ``pandas.Timedelta`` values will
|
||||
default to uneditable, but this can be changed through column
|
||||
configuration.
|
||||
|
||||
width : int or None
|
||||
Desired width of the data editor expressed in pixels. If ``width``
|
||||
is ``None`` (default), Streamlit sets the data editor width to fit
|
||||
its contents up to the width of the parent container. If ``width``
|
||||
is greater than the width of the parent container, Streamlit sets
|
||||
the data editor width to match the width of the parent container.
|
||||
|
||||
height : int or None
|
||||
Desired height of the data editor expressed in pixels. If ``height``
|
||||
is ``None`` (default), Streamlit sets the height to show at most
|
||||
ten rows. Vertical scrolling within the data editor element is
|
||||
enabled when the height does not accomodate all rows.
|
||||
|
||||
use_container_width : bool
|
||||
Whether to override ``width`` with the width of the parent
|
||||
container. If this is ``True`` (default), Streamlit sets the width
|
||||
of the data editor to match the width of the parent container. If
|
||||
this is ``False``, Streamlit sets the data editor's width according
|
||||
to ``width``.
|
||||
|
||||
hide_index : bool or None
|
||||
Whether to hide the index column(s). If ``hide_index`` is ``None``
|
||||
(default), the visibility of index columns is automatically
|
||||
determined based on the data.
|
||||
|
||||
column_order : Iterable of str or None
|
||||
Specifies the display order of columns. This also affects which columns are
|
||||
visible. For example, ``column_order=("col2", "col1")`` will display 'col2'
|
||||
first, followed by 'col1', and will hide all other non-index columns. If
|
||||
None (default), the order is inherited from the original data structure.
|
||||
|
||||
column_config : dict or None
|
||||
Configures how columns are displayed, e.g. their title, visibility, type, or
|
||||
format, as well as editing properties such as min/max value or step.
|
||||
This needs to be a dictionary where each key is a column name and the value
|
||||
is one of:
|
||||
|
||||
- ``None`` to hide the column.
|
||||
|
||||
- A string to set the display label of the column.
|
||||
|
||||
- One of the column types defined under ``st.column_config``, e.g.
|
||||
``st.column_config.NumberColumn("Dollar values”, format=”$ %d")`` to show
|
||||
a column as dollar amounts. See more info on the available column types
|
||||
and config options `here <https://docs.streamlit.io/develop/api-reference/data/st.column_config>`_.
|
||||
|
||||
To configure the index column(s), use ``_index`` as the column name.
|
||||
|
||||
num_rows : "fixed" or "dynamic"
|
||||
Specifies if the user can add and delete rows in the data editor.
|
||||
If "fixed", the user cannot add or delete rows. If "dynamic", the user can
|
||||
add and delete rows in the data editor, but column sorting is disabled.
|
||||
Defaults to "fixed".
|
||||
|
||||
disabled : bool or Iterable of str
|
||||
Controls the editing of columns. If True, editing is disabled for all columns.
|
||||
If an Iterable of column names is provided (e.g., ``disabled=("col1", "col2"))``,
|
||||
only the specified columns will be disabled for editing. If False (default),
|
||||
all columns that support editing are editable.
|
||||
|
||||
key : str
|
||||
An optional string to use as the unique key for this widget. If this
|
||||
is omitted, a key will be generated for the widget based on its
|
||||
content. No two widgets may have the same key.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this data_editor's value changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
row_height : int or None
|
||||
The height of each row in the data editor in pixels. If ``row_height``
|
||||
is ``None`` (default), Streamlit will use a default row height,
|
||||
which fits one line of text.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame, pandas.Series, pyarrow.Table, numpy.ndarray, list, set, tuple, or dict.
|
||||
The edited data. The edited data is returned in its original data type if
|
||||
it corresponds to any of the supported return types. All other data types
|
||||
are returned as a ``pandas.DataFrame``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> [
|
||||
>>> {"command": "st.selectbox", "rating": 4, "is_widget": True},
|
||||
>>> {"command": "st.balloons", "rating": 5, "is_widget": False},
|
||||
>>> {"command": "st.time_input", "rating": 3, "is_widget": True},
|
||||
>>> ]
|
||||
>>> )
|
||||
>>> edited_df = st.data_editor(df)
|
||||
>>>
|
||||
>>> favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
|
||||
>>> st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
|
||||
|
||||
.. output::
|
||||
https://doc-data-editor.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
You can also allow the user to add and delete rows by setting ``num_rows`` to "dynamic":
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> [
|
||||
>>> {"command": "st.selectbox", "rating": 4, "is_widget": True},
|
||||
>>> {"command": "st.balloons", "rating": 5, "is_widget": False},
|
||||
>>> {"command": "st.time_input", "rating": 3, "is_widget": True},
|
||||
>>> ]
|
||||
>>> )
|
||||
>>> edited_df = st.data_editor(df, num_rows="dynamic")
|
||||
>>>
|
||||
>>> favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
|
||||
>>> st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
|
||||
|
||||
.. output::
|
||||
https://doc-data-editor1.streamlit.app/
|
||||
height: 450px
|
||||
|
||||
Or you can customize the data editor via ``column_config``, ``hide_index``,
|
||||
``column_order``, or ``disabled``:
|
||||
|
||||
>>> import pandas as pd
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> df = pd.DataFrame(
|
||||
>>> [
|
||||
>>> {"command": "st.selectbox", "rating": 4, "is_widget": True},
|
||||
>>> {"command": "st.balloons", "rating": 5, "is_widget": False},
|
||||
>>> {"command": "st.time_input", "rating": 3, "is_widget": True},
|
||||
>>> ]
|
||||
>>> )
|
||||
>>> edited_df = st.data_editor(
|
||||
>>> df,
|
||||
>>> column_config={
|
||||
>>> "command": "Streamlit Command",
|
||||
>>> "rating": st.column_config.NumberColumn(
|
||||
>>> "Your rating",
|
||||
>>> help="How much do you like this command (1-5)?",
|
||||
>>> min_value=1,
|
||||
>>> max_value=5,
|
||||
>>> step=1,
|
||||
>>> format="%d ⭐",
|
||||
>>> ),
|
||||
>>> "is_widget": "Widget ?",
|
||||
>>> },
|
||||
>>> disabled=["command", "is_widget"],
|
||||
>>> hide_index=True,
|
||||
>>> )
|
||||
>>>
|
||||
>>> favorite_command = edited_df.loc[edited_df["rating"].idxmax()]["command"]
|
||||
>>> st.markdown(f"Your favorite command is **{favorite_command}** 🎈")
|
||||
|
||||
|
||||
.. output::
|
||||
https://doc-data-editor-config.streamlit.app/
|
||||
height: 350px
|
||||
|
||||
"""
|
||||
# Lazy-loaded import
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
)
|
||||
|
||||
if column_order is not None:
|
||||
column_order = list(column_order)
|
||||
|
||||
column_config_mapping: ColumnConfigMapping = {}
|
||||
|
||||
data_format = dataframe_util.determine_data_format(data)
|
||||
if data_format == dataframe_util.DataFormat.UNKNOWN:
|
||||
raise StreamlitAPIException(
|
||||
f"The data type ({type(data).__name__}) or format is not supported by "
|
||||
"the data editor. Please convert your data into a Pandas Dataframe or "
|
||||
"another supported data format."
|
||||
)
|
||||
|
||||
# The dataframe should always be a copy of the original data
|
||||
# since we will apply edits directly to it.
|
||||
data_df = dataframe_util.convert_anything_to_pandas_df(data, ensure_copy=True)
|
||||
|
||||
# Check if the index is supported.
|
||||
if not _is_supported_index(data_df.index):
|
||||
raise StreamlitAPIException(
|
||||
f"The type of the dataframe index - {type(data_df.index).__name__} - is not "
|
||||
"yet supported by the data editor."
|
||||
)
|
||||
|
||||
# Check if the column names are valid and unique.
|
||||
_check_column_names(data_df)
|
||||
|
||||
# Convert the user provided column config into the frontend compatible format:
|
||||
column_config_mapping = process_config_mapping(column_config)
|
||||
|
||||
# Deactivate editing for columns that are not compatible with arrow
|
||||
for column_name, column_data in data_df.items():
|
||||
if dataframe_util.is_colum_type_arrow_incompatible(column_data):
|
||||
update_column_config(
|
||||
column_config_mapping, column_name, {"disabled": True}
|
||||
)
|
||||
# Convert incompatible type to string
|
||||
data_df[column_name] = column_data.astype("string")
|
||||
|
||||
apply_data_specific_configs(column_config_mapping, data_format)
|
||||
|
||||
# Fix the column headers to work correctly for data editing:
|
||||
_fix_column_headers(data_df)
|
||||
|
||||
has_range_index = isinstance(data_df.index, pd.RangeIndex)
|
||||
|
||||
if not has_range_index:
|
||||
# If the index is not a range index, we will configure it as required
|
||||
# since the user is required to provide a (unique) value for editing.
|
||||
update_column_config(
|
||||
column_config_mapping, INDEX_IDENTIFIER, {"required": True}
|
||||
)
|
||||
|
||||
if hide_index is None and has_range_index and num_rows == "dynamic":
|
||||
# Temporary workaround:
|
||||
# We hide range indices if num_rows is dynamic.
|
||||
# since the current way of handling this index during editing is a
|
||||
# bit confusing. The user can still decide to show the index by
|
||||
# setting hide_index explicitly to False.
|
||||
hide_index = True
|
||||
|
||||
if hide_index is not None:
|
||||
update_column_config(
|
||||
column_config_mapping, INDEX_IDENTIFIER, {"hidden": hide_index}
|
||||
)
|
||||
|
||||
# If disabled not a boolean, we assume it is a list of columns to disable.
|
||||
# This gets translated into the columns configuration:
|
||||
if not isinstance(disabled, bool):
|
||||
for column in disabled:
|
||||
update_column_config(column_config_mapping, column, {"disabled": True})
|
||||
|
||||
# Convert the dataframe to an arrow table which is used as the main
|
||||
# serialization format for sending the data to the frontend.
|
||||
# We also utilize the arrow schema to determine the data kinds of every column.
|
||||
arrow_table = pa.Table.from_pandas(data_df)
|
||||
|
||||
# Determine the dataframe schema which is required for parsing edited values
|
||||
# and for checking type compatibilities.
|
||||
dataframe_schema = determine_dataframe_schema(data_df, arrow_table.schema)
|
||||
|
||||
# Check if all configured column types are compatible with the underlying data.
|
||||
# Throws an exception if any of the configured types are incompatible.
|
||||
_check_type_compatibilities(data_df, column_config_mapping, dataframe_schema)
|
||||
|
||||
arrow_bytes = dataframe_util.convert_arrow_table_to_arrow_bytes(arrow_table)
|
||||
|
||||
# We want to do this as early as possible to avoid introducing nondeterminism,
|
||||
# but it isn't clear how much processing is needed to have the data in a
|
||||
# format that will hash consistently, so we do it late here to have it
|
||||
# as close as possible to how it used to be.
|
||||
ctx = get_script_run_ctx()
|
||||
element_id = compute_and_register_element_id(
|
||||
"data_editor",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
data=arrow_bytes,
|
||||
width=width,
|
||||
height=height,
|
||||
use_container_width=use_container_width,
|
||||
column_order=column_order,
|
||||
column_config_mapping=str(column_config_mapping),
|
||||
num_rows=num_rows,
|
||||
row_height=row_height,
|
||||
)
|
||||
|
||||
proto = ArrowProto()
|
||||
proto.id = element_id
|
||||
|
||||
if use_container_width is None:
|
||||
# If use_container_width was not explicitly set by the user, we set
|
||||
# it to True if width was not set explicitly, and False otherwise.
|
||||
use_container_width = True if width is None else False
|
||||
|
||||
proto.use_container_width = use_container_width
|
||||
|
||||
if width:
|
||||
proto.width = width
|
||||
if height:
|
||||
proto.height = height
|
||||
|
||||
if row_height:
|
||||
proto.row_height = row_height
|
||||
|
||||
if column_order:
|
||||
proto.column_order[:] = column_order
|
||||
|
||||
# Only set disabled to true if it is actually true
|
||||
# It can also be a list of columns, which should result in false here.
|
||||
proto.disabled = disabled is True
|
||||
|
||||
proto.editing_mode = (
|
||||
ArrowProto.EditingMode.DYNAMIC
|
||||
if num_rows == "dynamic"
|
||||
else ArrowProto.EditingMode.FIXED
|
||||
)
|
||||
|
||||
proto.form_id = current_form_id(self.dg)
|
||||
|
||||
if dataframe_util.is_pandas_styler(data):
|
||||
# Pandas styler will only work for non-editable/disabled columns.
|
||||
# Get first 10 chars of md5 hash of the key or delta path as styler uuid
|
||||
# and set it as styler uuid.
|
||||
# We are only using the first 10 chars to keep the uuid short since
|
||||
# it will be used for all the cells in the dataframe. Therefore, this
|
||||
# might have a significant impact on the message size. 10 chars
|
||||
# should be good enough to avoid potential collisions in this case.
|
||||
# Even on collisions, there should not be a big issue with the
|
||||
# rendering in the data editor.
|
||||
styler_uuid = calc_md5(key or self.dg._get_delta_path_str())[:10]
|
||||
data.set_uuid(styler_uuid)
|
||||
marshall_styler(proto, data, styler_uuid)
|
||||
|
||||
proto.data = arrow_bytes
|
||||
|
||||
marshall_column_config(proto, column_config_mapping)
|
||||
|
||||
serde = DataEditorSerde()
|
||||
|
||||
widget_state = register_widget(
|
||||
proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="string_value",
|
||||
)
|
||||
|
||||
_apply_dataframe_edits(data_df, widget_state.value, dataframe_schema)
|
||||
self.dg._enqueue("arrow_data_frame", proto)
|
||||
return dataframe_util.convert_pandas_df_to_data_format(data_df, data_format)
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,486 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Literal, Union, cast, overload
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit import config
|
||||
from streamlit.elements.lib.file_uploader_utils import (
|
||||
enforce_filename_restriction,
|
||||
normalize_upload_file_type,
|
||||
)
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.proto.Common_pb2 import FileUploaderState as FileUploaderStateProto
|
||||
from streamlit.proto.Common_pb2 import UploadedFileInfo as UploadedFileInfoProto
|
||||
from streamlit.proto.FileUploader_pb2 import FileUploader as FileUploaderProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
register_widget,
|
||||
)
|
||||
from streamlit.runtime.uploaded_file_manager import DeletedFile, UploadedFile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
SomeUploadedFiles: TypeAlias = Union[
|
||||
UploadedFile,
|
||||
DeletedFile,
|
||||
list[Union[UploadedFile, DeletedFile]],
|
||||
None,
|
||||
]
|
||||
|
||||
|
||||
def _get_upload_files(
|
||||
widget_value: FileUploaderStateProto | None,
|
||||
) -> list[UploadedFile | DeletedFile]:
|
||||
if widget_value is None:
|
||||
return []
|
||||
|
||||
ctx = get_script_run_ctx()
|
||||
if ctx is None:
|
||||
return []
|
||||
|
||||
uploaded_file_info = widget_value.uploaded_file_info
|
||||
if len(uploaded_file_info) == 0:
|
||||
return []
|
||||
|
||||
file_recs_list = ctx.uploaded_file_mgr.get_files(
|
||||
session_id=ctx.session_id,
|
||||
file_ids=[f.file_id for f in uploaded_file_info],
|
||||
)
|
||||
|
||||
file_recs = {f.file_id: f for f in file_recs_list}
|
||||
|
||||
collected_files: list[UploadedFile | DeletedFile] = []
|
||||
|
||||
for f in uploaded_file_info:
|
||||
maybe_file_rec = file_recs.get(f.file_id)
|
||||
if maybe_file_rec is not None:
|
||||
uploaded_file = UploadedFile(maybe_file_rec, f.file_urls)
|
||||
collected_files.append(uploaded_file)
|
||||
else:
|
||||
collected_files.append(DeletedFile(f.file_id))
|
||||
|
||||
return collected_files
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileUploaderSerde:
|
||||
accept_multiple_files: bool
|
||||
allowed_types: Sequence[str] | None = None
|
||||
|
||||
def deserialize(
|
||||
self, ui_value: FileUploaderStateProto | None, widget_id: str
|
||||
) -> SomeUploadedFiles:
|
||||
upload_files = _get_upload_files(ui_value)
|
||||
|
||||
for file in upload_files:
|
||||
if isinstance(file, DeletedFile):
|
||||
continue
|
||||
|
||||
if self.allowed_types:
|
||||
enforce_filename_restriction(file.name, self.allowed_types)
|
||||
|
||||
if len(upload_files) == 0:
|
||||
return_value: SomeUploadedFiles = [] if self.accept_multiple_files else None
|
||||
else:
|
||||
return_value = (
|
||||
upload_files if self.accept_multiple_files else upload_files[0]
|
||||
)
|
||||
return return_value
|
||||
|
||||
def serialize(self, files: SomeUploadedFiles) -> FileUploaderStateProto:
|
||||
state_proto = FileUploaderStateProto()
|
||||
|
||||
if not files:
|
||||
return state_proto
|
||||
elif not isinstance(files, list):
|
||||
files = [files]
|
||||
|
||||
for f in files:
|
||||
if isinstance(f, DeletedFile):
|
||||
continue
|
||||
file_info: UploadedFileInfoProto = state_proto.uploaded_file_info.add()
|
||||
file_info.file_id = f.file_id
|
||||
file_info.name = f.name
|
||||
file_info.size = f.size
|
||||
file_info.file_urls.CopyFrom(f._file_urls)
|
||||
|
||||
return state_proto
|
||||
|
||||
|
||||
class FileUploaderMixin:
|
||||
# Multiple overloads are defined on `file_uploader()` below to represent
|
||||
# the different return types of `file_uploader()`.
|
||||
# These return types differ according to the value of the `accept_multiple_files` argument.
|
||||
# There are 2 associated variables, each with 2 options.
|
||||
# 1. The `accept_multiple_files` argument is set as `True`,
|
||||
# or it is set as `False` or omitted, in which case the default value `False`.
|
||||
# 2. The `type` argument may or may not be provided as a keyword-only argument.
|
||||
# There must be 2x2=4 overloads to cover all the possible arguments,
|
||||
# as these overloads must be mutually exclusive for mypy.
|
||||
|
||||
# 1. type is given as not a keyword-only argument
|
||||
# 2. accept_multiple_files = True
|
||||
@overload
|
||||
def file_uploader(
|
||||
self,
|
||||
label: str,
|
||||
type: str | Sequence[str] | None,
|
||||
accept_multiple_files: Literal[True],
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> list[UploadedFile] | None: ...
|
||||
|
||||
# 1. type is given as not a keyword-only argument
|
||||
# 2. accept_multiple_files = False or omitted
|
||||
@overload
|
||||
def file_uploader(
|
||||
self,
|
||||
label: str,
|
||||
type: str | Sequence[str] | None,
|
||||
accept_multiple_files: Literal[False] = False,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> UploadedFile | None: ...
|
||||
|
||||
# The following 2 overloads represent the cases where
|
||||
# the `type` argument is a keyword-only argument.
|
||||
# See https://github.com/python/mypy/issues/4020#issuecomment-737600893
|
||||
# for the related discussions and examples.
|
||||
|
||||
# 1. type is skipped or a keyword argument
|
||||
# 2. accept_multiple_files = True
|
||||
@overload
|
||||
def file_uploader(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
accept_multiple_files: Literal[True],
|
||||
type: str | Sequence[str] | None = None,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> list[UploadedFile] | None: ...
|
||||
|
||||
# 1. type is skipped or a keyword argument
|
||||
# 2. accept_multiple_files = False or omitted
|
||||
@overload
|
||||
def file_uploader(
|
||||
self,
|
||||
label: str,
|
||||
*,
|
||||
accept_multiple_files: Literal[False] = False,
|
||||
type: str | Sequence[str] | None = None,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> UploadedFile | None: ...
|
||||
|
||||
@gather_metrics("file_uploader")
|
||||
def file_uploader(
|
||||
self,
|
||||
label: str,
|
||||
type: str | Sequence[str] | None = None,
|
||||
accept_multiple_files: bool = False,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> UploadedFile | list[UploadedFile] | None:
|
||||
r"""Display a file uploader widget.
|
||||
By default, uploaded files are limited to 200 MB each. You can
|
||||
configure this using the ``server.maxUploadSize`` config option. For
|
||||
more information on how to set config options, see |config.toml|_.
|
||||
|
||||
.. |config.toml| replace:: ``config.toml``
|
||||
.. _config.toml: https://docs.streamlit.io/develop/api-reference/configuration/config.toml
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this file uploader is for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
type : str or list of str or None
|
||||
The allowed file extension(s) for uploaded files. This can be one
|
||||
of the following types:
|
||||
|
||||
- ``None`` (default): All file extensions are allowed.
|
||||
- A string: A single file extension is allowed. For example, to
|
||||
only accept CSV files, use ``"csv"``.
|
||||
- A sequence of strings: Multiple file extensions are allowed. For
|
||||
example, to only accept JPG/JPEG and PNG files, use
|
||||
``["jpg", "jpeg", "png"]``.
|
||||
|
||||
accept_multiple_files : bool
|
||||
Whether to accept more than one file in a submission. If this is
|
||||
``False`` (default), the user can only submit one file at a time.
|
||||
If this is ``True``, the user can upload multiple files at the same
|
||||
time, in which case the return value will be a list of files.
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this file_uploader's value
|
||||
changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the file uploader if set to
|
||||
``True``. The default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
None, UploadedFile, or list of UploadedFile
|
||||
- If accept_multiple_files is False, returns either None or
|
||||
an UploadedFile object.
|
||||
- If accept_multiple_files is True, returns a list with the
|
||||
uploaded files as UploadedFile objects. If no files were
|
||||
uploaded, returns an empty list.
|
||||
|
||||
The UploadedFile class is a subclass of BytesIO, and therefore is
|
||||
"file-like". This means you can pass an instance of it anywhere a
|
||||
file is expected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Insert a file uploader that accepts a single file at a time:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>> import pandas as pd
|
||||
>>> from io import StringIO
|
||||
>>>
|
||||
>>> uploaded_file = st.file_uploader("Choose a file")
|
||||
>>> if uploaded_file is not None:
|
||||
... # To read file as bytes:
|
||||
... bytes_data = uploaded_file.getvalue()
|
||||
... st.write(bytes_data)
|
||||
>>>
|
||||
... # To convert to a string based IO:
|
||||
... stringio = StringIO(uploaded_file.getvalue().decode("utf-8"))
|
||||
... st.write(stringio)
|
||||
>>>
|
||||
... # To read file as string:
|
||||
... string_data = stringio.read()
|
||||
... st.write(string_data)
|
||||
>>>
|
||||
... # Can be used wherever a "file-like" object is accepted:
|
||||
... dataframe = pd.read_csv(uploaded_file)
|
||||
... st.write(dataframe)
|
||||
|
||||
Insert a file uploader that accepts multiple files at a time:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> uploaded_files = st.file_uploader(
|
||||
... "Choose a CSV file", accept_multiple_files=True
|
||||
... )
|
||||
>>> for uploaded_file in uploaded_files:
|
||||
... bytes_data = uploaded_file.read()
|
||||
... st.write("filename:", uploaded_file.name)
|
||||
... st.write(bytes_data)
|
||||
|
||||
.. output::
|
||||
https://doc-file-uploader.streamlit.app/
|
||||
height: 375px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._file_uploader(
|
||||
label=label,
|
||||
type=type,
|
||||
accept_multiple_files=accept_multiple_files,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _file_uploader(
|
||||
self,
|
||||
label: str,
|
||||
type: str | Sequence[str] | None = None,
|
||||
accept_multiple_files: bool = False,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
disabled: bool = False,
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> UploadedFile | list[UploadedFile] | None:
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=None,
|
||||
writes_allowed=False,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"file_uploader",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
label=label,
|
||||
type=type,
|
||||
accept_multiple_files=accept_multiple_files,
|
||||
help=help,
|
||||
)
|
||||
|
||||
if type:
|
||||
type = normalize_upload_file_type(type)
|
||||
|
||||
file_uploader_proto = FileUploaderProto()
|
||||
file_uploader_proto.id = element_id
|
||||
file_uploader_proto.label = label
|
||||
file_uploader_proto.type[:] = type if type is not None else []
|
||||
file_uploader_proto.max_upload_size_mb = config.get_option(
|
||||
"server.maxUploadSize"
|
||||
)
|
||||
file_uploader_proto.multiple_files = accept_multiple_files
|
||||
file_uploader_proto.form_id = current_form_id(self.dg)
|
||||
file_uploader_proto.disabled = disabled
|
||||
file_uploader_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
if help is not None:
|
||||
file_uploader_proto.help = dedent(help)
|
||||
|
||||
serde = FileUploaderSerde(accept_multiple_files, allowed_types=type)
|
||||
|
||||
# FileUploader's widget value is a list of file IDs
|
||||
# representing the current set of files that this uploader should
|
||||
# know about.
|
||||
widget_state = register_widget(
|
||||
file_uploader_proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="file_uploader_state_value",
|
||||
)
|
||||
|
||||
self.dg._enqueue("file_uploader", file_uploader_proto)
|
||||
|
||||
if isinstance(widget_state.value, DeletedFile):
|
||||
return None
|
||||
elif isinstance(widget_state.value, list):
|
||||
return [f for f in widget_state.value if not isinstance(f, DeletedFile)]
|
||||
|
||||
return widget_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,339 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Any, Callable, Generic, cast
|
||||
|
||||
from streamlit.dataframe_util import OptionSequence
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.options_selector_utils import (
|
||||
check_and_convert_to_indices,
|
||||
convert_to_sequence_and_check_comparable,
|
||||
get_default_indices,
|
||||
maybe_coerce_enum_sequence,
|
||||
)
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
save_for_app_testing,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.errors import (
|
||||
StreamlitSelectionCountExceedsMaxError,
|
||||
)
|
||||
from streamlit.proto.MultiSelect_pb2 import MultiSelect as MultiSelectProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import register_widget
|
||||
from streamlit.type_util import (
|
||||
T,
|
||||
is_iterable,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
from streamlit.dataframe_util import OptionSequence
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MultiSelectSerde(Generic[T]):
|
||||
options: Sequence[T]
|
||||
default_value: list[int] = field(default_factory=list)
|
||||
|
||||
def serialize(self, value: list[T]) -> list[int]:
|
||||
indices = check_and_convert_to_indices(self.options, value)
|
||||
return indices if indices is not None else []
|
||||
|
||||
def deserialize(
|
||||
self,
|
||||
ui_value: list[int] | None,
|
||||
widget_id: str = "",
|
||||
) -> list[T]:
|
||||
current_value: list[int] = (
|
||||
ui_value if ui_value is not None else self.default_value
|
||||
)
|
||||
return [self.options[i] for i in current_value]
|
||||
|
||||
|
||||
def _get_default_count(default: Sequence[Any] | Any | None) -> int:
|
||||
if default is None:
|
||||
return 0
|
||||
if not is_iterable(default):
|
||||
return 1
|
||||
return len(cast("Sequence[Any]", default))
|
||||
|
||||
|
||||
def _check_max_selections(
|
||||
selections: Sequence[Any] | Any | None, max_selections: int | None
|
||||
):
|
||||
if max_selections is None:
|
||||
return
|
||||
|
||||
default_count = _get_default_count(selections)
|
||||
if default_count > max_selections:
|
||||
raise StreamlitSelectionCountExceedsMaxError(
|
||||
current_selections_count=default_count, max_selections_count=max_selections
|
||||
)
|
||||
|
||||
|
||||
class MultiSelectMixin:
|
||||
@gather_metrics("multiselect")
|
||||
def multiselect(
|
||||
self,
|
||||
label: str,
|
||||
options: OptionSequence[T],
|
||||
default: Any | None = None,
|
||||
format_func: Callable[[Any], Any] = str,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
max_selections: int | None = None,
|
||||
placeholder: str = "Choose an option",
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> list[T]:
|
||||
r"""Display a multiselect widget.
|
||||
The multiselect widget starts as empty.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label: str
|
||||
A short label explaining to the user what this select widget is for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
options: Iterable
|
||||
Labels for the select options in an ``Iterable``. This can be a
|
||||
``list``, ``set``, or anything supported by ``st.dataframe``. If
|
||||
``options`` is dataframe-like, the first column will be used. Each
|
||||
label will be cast to ``str`` internally by default.
|
||||
|
||||
default: Iterable of V, V, or None
|
||||
List of default values. Can also be a single value.
|
||||
|
||||
format_func: function
|
||||
Function to modify the display of the options. It receives
|
||||
the raw option as an argument and should output the label to be
|
||||
shown for that option. This has no impact on the return value of
|
||||
the command.
|
||||
|
||||
key: str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help: str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change: callable
|
||||
An optional callback invoked when this widget's value changes.
|
||||
|
||||
args: tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs: dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
max_selections: int
|
||||
The max selections that can be selected at a time.
|
||||
|
||||
placeholder: str
|
||||
A string to display when no options are selected.
|
||||
Defaults to "Choose an option."
|
||||
|
||||
disabled: bool
|
||||
An optional boolean that disables the multiselect widget if set
|
||||
to ``True``. The default is ``False``.
|
||||
|
||||
label_visibility: "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list
|
||||
A list with the selected options
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> options = st.multiselect(
|
||||
... "What are your favorite colors",
|
||||
... ["Green", "Yellow", "Red", "Blue"],
|
||||
... ["Yellow", "Red"],
|
||||
... )
|
||||
>>>
|
||||
>>> st.write("You selected:", options)
|
||||
|
||||
.. output::
|
||||
https://doc-multiselect.streamlit.app/
|
||||
height: 420px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._multiselect(
|
||||
label=label,
|
||||
options=options,
|
||||
default=default,
|
||||
format_func=format_func,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
max_selections=max_selections,
|
||||
placeholder=placeholder,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _multiselect(
|
||||
self,
|
||||
label: str,
|
||||
options: OptionSequence[T],
|
||||
default: Sequence[Any] | Any | None = None,
|
||||
format_func: Callable[[Any], Any] = str,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
max_selections: int | None = None,
|
||||
placeholder: str = "Choose an option",
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> list[T]:
|
||||
key = to_key(key)
|
||||
|
||||
widget_name = "multiselect"
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=default,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
indexable_options = convert_to_sequence_and_check_comparable(options)
|
||||
formatted_options = [format_func(option) for option in indexable_options]
|
||||
default_values = get_default_indices(indexable_options, default)
|
||||
|
||||
form_id = current_form_id(self.dg)
|
||||
element_id = compute_and_register_element_id(
|
||||
widget_name,
|
||||
user_key=key,
|
||||
form_id=form_id,
|
||||
label=label,
|
||||
options=formatted_options,
|
||||
default=default_values,
|
||||
help=help,
|
||||
max_selections=max_selections,
|
||||
placeholder=placeholder,
|
||||
)
|
||||
|
||||
proto = MultiSelectProto()
|
||||
proto.id = element_id
|
||||
proto.default[:] = default_values
|
||||
proto.form_id = form_id
|
||||
proto.disabled = disabled
|
||||
proto.label = label
|
||||
proto.max_selections = max_selections or 0
|
||||
proto.placeholder = placeholder
|
||||
proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
proto.options[:] = formatted_options
|
||||
if help is not None:
|
||||
proto.help = dedent(help)
|
||||
|
||||
serde = MultiSelectSerde(indexable_options, default_values)
|
||||
widget_state = register_widget(
|
||||
proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="int_array_value",
|
||||
)
|
||||
|
||||
_check_max_selections(widget_state.value, max_selections)
|
||||
widget_state = maybe_coerce_enum_sequence(
|
||||
widget_state, options, indexable_options
|
||||
)
|
||||
|
||||
if widget_state.value_changed:
|
||||
proto.value[:] = serde.serialize(widget_state.value)
|
||||
proto.set_value = True
|
||||
|
||||
if ctx:
|
||||
save_for_app_testing(ctx, element_id, format_func)
|
||||
|
||||
self.dg._enqueue(widget_name, proto)
|
||||
|
||||
return widget_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
@@ -0,0 +1,562 @@
|
||||
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numbers
|
||||
from dataclasses import dataclass
|
||||
from textwrap import dedent
|
||||
from typing import TYPE_CHECKING, Literal, TypeVar, Union, cast, overload
|
||||
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from streamlit.elements.lib.form_utils import current_form_id
|
||||
from streamlit.elements.lib.js_number import JSNumber, JSNumberBoundsException
|
||||
from streamlit.elements.lib.policies import (
|
||||
check_widget_policies,
|
||||
maybe_raise_label_warnings,
|
||||
)
|
||||
from streamlit.elements.lib.utils import (
|
||||
Key,
|
||||
LabelVisibility,
|
||||
compute_and_register_element_id,
|
||||
get_label_visibility_proto_value,
|
||||
to_key,
|
||||
)
|
||||
from streamlit.errors import (
|
||||
StreamlitInvalidNumberFormatError,
|
||||
StreamlitJSNumberBoundsError,
|
||||
StreamlitMixedNumericTypesError,
|
||||
StreamlitValueAboveMaxError,
|
||||
StreamlitValueBelowMinError,
|
||||
)
|
||||
from streamlit.proto.NumberInput_pb2 import NumberInput as NumberInputProto
|
||||
from streamlit.runtime.metrics_util import gather_metrics
|
||||
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
|
||||
from streamlit.runtime.state import (
|
||||
WidgetArgs,
|
||||
WidgetCallback,
|
||||
WidgetKwargs,
|
||||
get_session_state,
|
||||
register_widget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
|
||||
Number: TypeAlias = Union[int, float]
|
||||
IntOrNone = TypeVar("IntOrNone", int, None)
|
||||
FloatOrNone = TypeVar("FloatOrNone", float, None)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NumberInputSerde:
|
||||
value: Number | None
|
||||
data_type: int
|
||||
|
||||
def serialize(self, v: Number | None) -> Number | None:
|
||||
return v
|
||||
|
||||
def deserialize(
|
||||
self, ui_value: Number | None, widget_id: str = ""
|
||||
) -> Number | None:
|
||||
val: Number | None = ui_value if ui_value is not None else self.value
|
||||
|
||||
if val is not None and self.data_type == NumberInputProto.INT:
|
||||
val = int(val)
|
||||
|
||||
return val
|
||||
|
||||
|
||||
class NumberInputMixin:
|
||||
# For easier readability, all the arguments with un-changing types across these overload signatures have been
|
||||
# collapsed onto a single line.
|
||||
|
||||
# fmt: off
|
||||
# If "min_value: int" is given and all other numerical inputs are
|
||||
# "int"s or not provided (value optionally being "min"), return "int"
|
||||
# If "min_value: int, value: None" is given and all other numerical inputs
|
||||
# are "int"s or not provided, return "int | None"
|
||||
@overload
|
||||
def number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: int,
|
||||
max_value: int | None = None,
|
||||
value: IntOrNone | Literal["min"] = "min",
|
||||
step: int | None = None,
|
||||
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
|
||||
) -> int | IntOrNone:
|
||||
...
|
||||
|
||||
# If "max_value: int" is given and all other numerical inputs are
|
||||
# "int"s or not provided (value optionally being "min"), return "int"
|
||||
# If "max_value: int, value=None" is given and all other numerical inputs
|
||||
# are "int"s or not provided, return "int | None"
|
||||
@overload
|
||||
def number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: int | None = None,
|
||||
*,
|
||||
max_value: int,
|
||||
value: IntOrNone | Literal["min"] = "min",
|
||||
step: int | None = None,
|
||||
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
|
||||
) -> int | IntOrNone:
|
||||
...
|
||||
|
||||
# If "value=int" is given and all other numerical inputs are "int"s
|
||||
# or not provided, return "int"
|
||||
@overload
|
||||
def number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: int | None = None,
|
||||
max_value: int | None = None,
|
||||
*,
|
||||
value: int,
|
||||
step: int | None = None,
|
||||
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
|
||||
) -> int:
|
||||
...
|
||||
|
||||
# If "step=int" is given and all other numerical inputs are "int"s
|
||||
# or not provided (value optionally being "min"), return "int"
|
||||
# If "step=int, value=None" is given and all other numerical inputs
|
||||
# are "int"s or not provided, return "int | None"
|
||||
@overload
|
||||
def number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: int | None = None,
|
||||
max_value: int | None = None,
|
||||
value: IntOrNone | Literal["min"] = "min",
|
||||
*,
|
||||
step: int,
|
||||
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
|
||||
) -> int | IntOrNone:
|
||||
...
|
||||
|
||||
# If all numerical inputs are floats (with value optionally being "min")
|
||||
# or are not provided, return "float"
|
||||
# If only "value=None" is given and none of the other numerical inputs
|
||||
# are "int"s, return "float | None"
|
||||
@overload
|
||||
def number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: float | None = None,
|
||||
max_value: float | None = None,
|
||||
value: FloatOrNone | Literal["min"] = "min",
|
||||
step: float | None = None,
|
||||
format: str | None = None, key: Key | None = None, help: str | None = None, on_change: WidgetCallback | None = None, args: WidgetArgs | None = None, kwargs: WidgetKwargs | None = None, *, placeholder: str | None = None, disabled: bool = False, label_visibility: LabelVisibility = "visible"
|
||||
) -> float | FloatOrNone:
|
||||
...
|
||||
# # fmt: on
|
||||
|
||||
@gather_metrics("number_input")
|
||||
def number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: Number | None = None,
|
||||
max_value: Number | None = None,
|
||||
value: Number | Literal["min"] | None = "min",
|
||||
step: Number | None = None,
|
||||
format: str | None = None,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
placeholder: str | None = None,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
) -> Number | None:
|
||||
r"""Display a numeric input widget.
|
||||
|
||||
.. note::
|
||||
Integer values exceeding +/- ``(1<<53) - 1`` cannot be accurately
|
||||
stored or returned by the widget due to serialization contstraints
|
||||
between the Python server and JavaScript client. You must handle
|
||||
such numbers as floats, leading to a loss in precision.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
A short label explaining to the user what this input is for.
|
||||
The label can optionally contain GitHub-flavored Markdown of the
|
||||
following types: Bold, Italics, Strikethroughs, Inline Code, Links,
|
||||
and Images. Images display like icons, with a max height equal to
|
||||
the font height.
|
||||
|
||||
Unsupported Markdown elements are unwrapped so only their children
|
||||
(text contents) render. Display unsupported elements as literal
|
||||
characters by backslash-escaping them. E.g.,
|
||||
``"1\. Not an ordered list"``.
|
||||
|
||||
See the ``body`` parameter of |st.markdown|_ for additional,
|
||||
supported Markdown directives.
|
||||
|
||||
For accessibility reasons, you should never set an empty label, but
|
||||
you can hide it with ``label_visibility`` if needed. In the future,
|
||||
we may disallow empty labels by raising an exception.
|
||||
|
||||
.. |st.markdown| replace:: ``st.markdown``
|
||||
.. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown
|
||||
|
||||
min_value : int, float, or None
|
||||
The minimum permitted value.
|
||||
If this is ``None`` (default), there will be no minimum for float
|
||||
values and a minimum of ``- (1<<53) + 1`` for integer values.
|
||||
|
||||
max_value : int, float, or None
|
||||
The maximum permitted value.
|
||||
If this is ``None`` (default), there will be no maximum for float
|
||||
values and a maximum of ``(1<<53) - 1`` for integer values.
|
||||
|
||||
value : int, float, "min" or None
|
||||
The value of this widget when it first renders. If this is
|
||||
``"min"`` (default), the initial value is ``min_value`` unless
|
||||
``min_value`` is ``None``. If ``min_value`` is ``None``, the widget
|
||||
initializes with a value of ``0.0`` or ``0``.
|
||||
|
||||
If ``value`` is ``None``, the widget will initialize with no value
|
||||
and return ``None`` until the user provides input.
|
||||
|
||||
step : int, float, or None
|
||||
The stepping interval.
|
||||
Defaults to 1 if the value is an int, 0.01 otherwise.
|
||||
If the value is not specified, the format parameter will be used.
|
||||
|
||||
format : str or None
|
||||
A printf-style format string controlling how the interface should
|
||||
display numbers. The output must be purely numeric. This does not
|
||||
impact the return value of the widget. For more information about
|
||||
the formatting specification, see `sprintf.js
|
||||
<https://github.com/alexei/sprintf.js?tab=readme-ov-file#format-specification>`_.
|
||||
|
||||
For example, ``format="%0.1f"`` adjusts the displayed decimal
|
||||
precision to only show one digit after the decimal.
|
||||
|
||||
key : str or int
|
||||
An optional string or integer to use as the unique key for the widget.
|
||||
If this is omitted, a key will be generated for the widget
|
||||
based on its content. No two widgets may have the same key.
|
||||
|
||||
help : str or None
|
||||
A tooltip that gets displayed next to the widget label. Streamlit
|
||||
only displays the tooltip when ``label_visibility="visible"``. If
|
||||
this is ``None`` (default), no tooltip is displayed.
|
||||
|
||||
The tooltip can optionally contain GitHub-flavored Markdown,
|
||||
including the Markdown directives described in the ``body``
|
||||
parameter of ``st.markdown``.
|
||||
|
||||
on_change : callable
|
||||
An optional callback invoked when this number_input's value changes.
|
||||
|
||||
args : tuple
|
||||
An optional tuple of args to pass to the callback.
|
||||
|
||||
kwargs : dict
|
||||
An optional dict of kwargs to pass to the callback.
|
||||
|
||||
placeholder : str or None
|
||||
An optional string displayed when the number input is empty.
|
||||
If None, no placeholder is displayed.
|
||||
|
||||
disabled : bool
|
||||
An optional boolean that disables the number input if set to
|
||||
``True``. The default is ``False``.
|
||||
|
||||
label_visibility : "visible", "hidden", or "collapsed"
|
||||
The visibility of the label. The default is ``"visible"``. If this
|
||||
is ``"hidden"``, Streamlit displays an empty spacer instead of the
|
||||
label, which can help keep the widget alligned with other widgets.
|
||||
If this is ``"collapsed"``, Streamlit displays no label or spacer.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int or float or None
|
||||
The current value of the numeric input widget or ``None`` if the widget
|
||||
is empty. The return type will match the data type of the value parameter.
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> number = st.number_input("Insert a number")
|
||||
>>> st.write("The current number is ", number)
|
||||
|
||||
.. output::
|
||||
https://doc-number-input.streamlit.app/
|
||||
height: 260px
|
||||
|
||||
To initialize an empty number input, use ``None`` as the value:
|
||||
|
||||
>>> import streamlit as st
|
||||
>>>
|
||||
>>> number = st.number_input(
|
||||
... "Insert a number", value=None, placeholder="Type a number..."
|
||||
... )
|
||||
>>> st.write("The current number is ", number)
|
||||
|
||||
.. output::
|
||||
https://doc-number-input-empty.streamlit.app/
|
||||
height: 260px
|
||||
|
||||
"""
|
||||
ctx = get_script_run_ctx()
|
||||
return self._number_input(
|
||||
label=label,
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
value=value,
|
||||
step=step,
|
||||
format=format,
|
||||
key=key,
|
||||
help=help,
|
||||
on_change=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
placeholder=placeholder,
|
||||
disabled=disabled,
|
||||
label_visibility=label_visibility,
|
||||
ctx=ctx,
|
||||
)
|
||||
|
||||
def _number_input(
|
||||
self,
|
||||
label: str,
|
||||
min_value: Number | None = None,
|
||||
max_value: Number | None = None,
|
||||
value: Number | Literal["min"] | None = "min",
|
||||
step: Number | None = None,
|
||||
format: str | None = None,
|
||||
key: Key | None = None,
|
||||
help: str | None = None,
|
||||
on_change: WidgetCallback | None = None,
|
||||
args: WidgetArgs | None = None,
|
||||
kwargs: WidgetKwargs | None = None,
|
||||
*, # keyword-only arguments:
|
||||
placeholder: str | None = None,
|
||||
disabled: bool = False,
|
||||
label_visibility: LabelVisibility = "visible",
|
||||
ctx: ScriptRunContext | None = None,
|
||||
) -> Number | None:
|
||||
key = to_key(key)
|
||||
|
||||
check_widget_policies(
|
||||
self.dg,
|
||||
key,
|
||||
on_change,
|
||||
default_value=value if value != "min" else None,
|
||||
)
|
||||
maybe_raise_label_warnings(label, label_visibility)
|
||||
|
||||
element_id = compute_and_register_element_id(
|
||||
"number_input",
|
||||
user_key=key,
|
||||
form_id=current_form_id(self.dg),
|
||||
label=label,
|
||||
min_value=min_value,
|
||||
max_value=max_value,
|
||||
value=value,
|
||||
step=step,
|
||||
format=format,
|
||||
help=help,
|
||||
placeholder=None if placeholder is None else str(placeholder),
|
||||
)
|
||||
|
||||
# Ensure that all arguments are of the same type.
|
||||
number_input_args = [min_value, max_value, value, step]
|
||||
|
||||
all_int_args = all(
|
||||
isinstance(a, (numbers.Integral, type(None), str))
|
||||
for a in number_input_args
|
||||
)
|
||||
|
||||
all_float_args = all(
|
||||
isinstance(a, (float, type(None), str)) for a in number_input_args
|
||||
)
|
||||
|
||||
if not all_int_args and not all_float_args:
|
||||
raise StreamlitMixedNumericTypesError(value=value, min_value=min_value, max_value=max_value, step=step)
|
||||
|
||||
session_state = get_session_state().filtered_state
|
||||
if key is not None and key in session_state and session_state[key] is None:
|
||||
value = None
|
||||
|
||||
if value == "min":
|
||||
if min_value is not None:
|
||||
value = min_value
|
||||
elif all_int_args and all_float_args:
|
||||
value = 0.0 # if no values are provided, defaults to float
|
||||
elif all_int_args:
|
||||
value = 0
|
||||
else:
|
||||
value = 0.0
|
||||
|
||||
int_value = isinstance(value, numbers.Integral)
|
||||
float_value = isinstance(value, float)
|
||||
|
||||
if value is None:
|
||||
if all_int_args and not all_float_args:
|
||||
# Select int type if all relevant args are ints:
|
||||
int_value = True
|
||||
else:
|
||||
# Otherwise, defaults to float:
|
||||
float_value = True
|
||||
|
||||
if format is None:
|
||||
format = "%d" if int_value else "%0.2f"
|
||||
|
||||
# Warn user if they format an int type as a float or vice versa.
|
||||
if format in ["%d", "%u", "%i"] and float_value:
|
||||
import streamlit as st
|
||||
|
||||
st.warning(
|
||||
"Warning: NumberInput value below has type float,"
|
||||
f" but format {format} displays as integer."
|
||||
)
|
||||
elif format[-1] == "f" and int_value:
|
||||
import streamlit as st
|
||||
|
||||
st.warning(
|
||||
"Warning: NumberInput value below has type int so is"
|
||||
f" displayed as int despite format string {format}."
|
||||
)
|
||||
|
||||
if step is None:
|
||||
step = 1 if int_value else 0.01
|
||||
|
||||
try:
|
||||
float(format % 2)
|
||||
except (TypeError, ValueError):
|
||||
raise StreamlitInvalidNumberFormatError(format)
|
||||
|
||||
|
||||
# Ensure that the value matches arguments' types.
|
||||
all_ints = int_value and all_int_args
|
||||
|
||||
if min_value is not None and value is not None and min_value > value:
|
||||
raise StreamlitValueBelowMinError(value=value, min_value=min_value)
|
||||
|
||||
if max_value is not None and value is not None and max_value < value:
|
||||
raise StreamlitValueAboveMaxError(value=value, max_value=max_value)
|
||||
|
||||
# Bounds checks. JSNumber produces human-readable exceptions that
|
||||
# we simply re-package as StreamlitAPIExceptions.
|
||||
try:
|
||||
if all_ints:
|
||||
if min_value is not None:
|
||||
JSNumber.validate_int_bounds(int(min_value), "`min_value`")
|
||||
else:
|
||||
# Issue 6740: If min_value not provided, set default to minimum safe integer
|
||||
# to avoid JS issues from smaller numbers entered via UI
|
||||
min_value = JSNumber.MIN_SAFE_INTEGER
|
||||
if max_value is not None:
|
||||
JSNumber.validate_int_bounds(int(max_value), "`max_value`")
|
||||
else:
|
||||
# See note above - set default to max safe integer
|
||||
max_value = JSNumber.MAX_SAFE_INTEGER
|
||||
if step is not None:
|
||||
JSNumber.validate_int_bounds(int(step), "`step`")
|
||||
if value is not None:
|
||||
JSNumber.validate_int_bounds(int(value), "`value`")
|
||||
else:
|
||||
if min_value is not None:
|
||||
JSNumber.validate_float_bounds(min_value, "`min_value`")
|
||||
else:
|
||||
# See note above
|
||||
min_value = JSNumber.MIN_NEGATIVE_VALUE
|
||||
if max_value is not None:
|
||||
JSNumber.validate_float_bounds(max_value, "`max_value`")
|
||||
else:
|
||||
# See note above
|
||||
max_value = JSNumber.MAX_VALUE
|
||||
if step is not None:
|
||||
JSNumber.validate_float_bounds(step, "`step`")
|
||||
if value is not None:
|
||||
JSNumber.validate_float_bounds(value, "`value`")
|
||||
except JSNumberBoundsException as e:
|
||||
raise StreamlitJSNumberBoundsError(str(e))
|
||||
|
||||
data_type = NumberInputProto.INT if all_ints else NumberInputProto.FLOAT
|
||||
|
||||
number_input_proto = NumberInputProto()
|
||||
number_input_proto.id = element_id
|
||||
number_input_proto.data_type = data_type
|
||||
number_input_proto.label = label
|
||||
if value is not None:
|
||||
number_input_proto.default = value
|
||||
if placeholder is not None:
|
||||
number_input_proto.placeholder = str(placeholder)
|
||||
number_input_proto.form_id = current_form_id(self.dg)
|
||||
number_input_proto.disabled = disabled
|
||||
number_input_proto.label_visibility.value = get_label_visibility_proto_value(
|
||||
label_visibility
|
||||
)
|
||||
|
||||
if help is not None:
|
||||
number_input_proto.help = dedent(help)
|
||||
|
||||
if min_value is not None:
|
||||
number_input_proto.min = min_value
|
||||
number_input_proto.has_min = True
|
||||
|
||||
if max_value is not None:
|
||||
number_input_proto.max = max_value
|
||||
number_input_proto.has_max = True
|
||||
|
||||
if step is not None:
|
||||
number_input_proto.step = step
|
||||
|
||||
if format is not None:
|
||||
number_input_proto.format = format
|
||||
|
||||
serde = NumberInputSerde(value, data_type)
|
||||
widget_state = register_widget(
|
||||
number_input_proto.id,
|
||||
on_change_handler=on_change,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
deserializer=serde.deserialize,
|
||||
serializer=serde.serialize,
|
||||
ctx=ctx,
|
||||
value_type="double_value"
|
||||
)
|
||||
|
||||
if widget_state.value_changed:
|
||||
if widget_state.value is not None:
|
||||
# Min/Max bounds checks when the value is updated.
|
||||
if number_input_proto.has_min and widget_state.value < number_input_proto.min:
|
||||
raise StreamlitValueBelowMinError(value=widget_state.value, min_value=number_input_proto.min)
|
||||
|
||||
if number_input_proto.has_max and widget_state.value > number_input_proto.max:
|
||||
raise StreamlitValueAboveMaxError(value=widget_state.value, max_value=number_input_proto.max)
|
||||
|
||||
number_input_proto.value = widget_state.value
|
||||
number_input_proto.set_value = True
|
||||
|
||||
self.dg._enqueue("number_input", number_input_proto)
|
||||
return widget_state.value
|
||||
|
||||
@property
|
||||
def dg(self) -> DeltaGenerator:
|
||||
"""Get our DeltaGenerator."""
|
||||
return cast("DeltaGenerator", self)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user