Mise à jour de Monitor.py et autres scripts

This commit is contained in:
Debian
2025-07-23 10:46:27 +02:00
parent 7081418ce0
commit 7de3e0fb50
8604 changed files with 2789953 additions and 295 deletions

View File

@@ -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.

View 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

View File

@@ -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()

View File

@@ -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

View 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)

View File

@@ -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

View File

@@ -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}