team-10/env/Lib/site-packages/streamlit/runtime/context.py

458 lines
15 KiB
Python
Raw Normal View History

2025-08-02 07:34:44 +02:00
# 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 Iterable, Iterator, Mapping
from functools import lru_cache
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Literal, cast
from streamlit import runtime
from streamlit.runtime.context_util import maybe_add_page_path, maybe_trim_page_path
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
from streamlit.util import AttributeDictionary
if TYPE_CHECKING:
from http.cookies import Morsel
from tornado.httputil import HTTPHeaders, HTTPServerRequest
from tornado.web import RequestHandler
def _get_request() -> HTTPServerRequest | None:
ctx = get_script_run_ctx()
if ctx is None:
return None
session_client = runtime.get_instance().get_client(ctx.session_id)
if session_client is None:
return None
# We return websocket request only if session_client is an instance of
# BrowserWebSocketHandler (which is True for the Streamlit open-source
# implementation). For any other implementation, we return None.
# We are not using `type_util.is_type` here to avoid circular import.
if (
f"{type(session_client).__module__}.{type(session_client).__qualname__}"
!= "streamlit.web.server.browser_websocket_handler.BrowserWebSocketHandler"
):
return None
return cast("RequestHandler", session_client).request
@lru_cache
def _normalize_header(name: str) -> str:
"""Map a header name to Http-Header-Case.
>>> _normalize_header("coNtent-TYPE")
'Content-Type'
"""
return "-".join(w.capitalize() for w in name.split("-"))
class StreamlitTheme(AttributeDictionary):
"""A dictionary-like object containing theme information.
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.
"""
type: Literal["dark", "light"] | None
def __init__(self, theme_info: dict[str, str | None]):
super().__init__(theme_info)
@classmethod
def from_context_info(cls, context_dict: dict[str, str | None]) -> StreamlitTheme:
return cls(context_dict)
class StreamlitHeaders(Mapping[str, str]):
def __init__(self, headers: Iterable[tuple[str, str]]) -> None:
dict_like_headers: dict[str, list[str]] = {}
for key, value in headers:
header_value = dict_like_headers.setdefault(_normalize_header(key), [])
header_value.append(value)
self._headers = dict_like_headers
@classmethod
def from_tornado_headers(cls, tornado_headers: HTTPHeaders) -> StreamlitHeaders:
return cls(tornado_headers.get_all())
def get_all(self, key: str) -> list[str]:
return list(self._headers.get(_normalize_header(key), []))
def __getitem__(self, key: str) -> str:
try:
return self._headers[_normalize_header(key)][0]
except LookupError:
raise KeyError(key) from None
def __len__(self) -> int:
"""Number of unique headers present in request."""
return len(self._headers)
def __iter__(self) -> Iterator[str]:
return iter(self._headers)
def to_dict(self) -> dict[str, str]:
return {key: self[key] for key in self}
class StreamlitCookies(Mapping[str, str]):
def __init__(self, cookies: Mapping[str, str]) -> None:
self._cookies = MappingProxyType(cookies)
@classmethod
def from_tornado_cookies(
cls, tornado_cookies: dict[str, Morsel[Any]]
) -> StreamlitCookies:
dict_like_cookies = {}
for key, morsel in tornado_cookies.items():
dict_like_cookies[key] = morsel.value
return cls(dict_like_cookies)
def __getitem__(self, key: str) -> str:
return self._cookies[key]
def __len__(self) -> int:
"""Number of unique headers present in request."""
return len(self._cookies)
def __iter__(self) -> Iterator[str]:
return iter(self._cookies)
def to_dict(self) -> dict[str, str]:
return dict(self._cookies)
class ContextProxy:
"""An interface to access user session context.
``st.context`` provides a read-only interface to access headers and cookies
for the current user session.
Each property (``st.context.headers`` and ``st.context.cookies``) returns
a dictionary of named values.
"""
@property
@gather_metrics("context.headers")
def headers(self) -> StreamlitHeaders:
"""A read-only, dict-like object containing headers sent in the initial request.
Keys are case-insensitive and may be repeated. When keys are repeated,
dict-like methods will only return the last instance of each key. Use
``.get_all(key="your_repeated_key")`` to see all values if the same
header is set multiple times.
Examples
--------
**Example 1: Access all available headers**
Show a dictionary of headers (with only the last instance of any
repeated key):
>>> import streamlit as st
>>>
>>> st.context.headers
**Example 2: Access a specific header**
Show the value of a specific header (or the last instance if it's
repeated):
>>> import streamlit as st
>>>
>>> st.context.headers["host"]
Show of list of all headers for a given key:
>>> import streamlit as st
>>>
>>> st.context.headers.get_all("pragma")
"""
# We have a docstring in line above as one-liner, to have a correct docstring
# in the st.write(st,context) call.
session_client_request = _get_request()
if session_client_request is None:
return StreamlitHeaders({})
return StreamlitHeaders.from_tornado_headers(session_client_request.headers)
@property
@gather_metrics("context.cookies")
def cookies(self) -> StreamlitCookies:
"""A read-only, dict-like object containing cookies sent in the initial request.
Examples
--------
**Example 1: Access all available cookies**
Show a dictionary of cookies:
>>> import streamlit as st
>>>
>>> st.context.cookies
**Example 2: Access a specific cookie**
Show the value of a specific cookie:
>>> import streamlit as st
>>>
>>> st.context.cookies["_ga"]
"""
# We have a docstring in line above as one-liner, to have a correct docstring
# in the st.write(st,context) call.
session_client_request = _get_request()
if session_client_request is None:
return StreamlitCookies({})
cookies = session_client_request.cookies
return StreamlitCookies.from_tornado_cookies(cookies)
@property
@gather_metrics("context.theme")
def theme(self) -> StreamlitTheme:
"""A read-only, dictionary-like object containing theme information.
Theme information is restricted to the ``type`` of the theme (dark or
light) and is inferred from the background color of the app.
.. note::
Changes made to the background color through CSS are not included.
Additionally, the theme type may be incorrect during a change in
theme, like in the following situations:
- When the app is first loaded within a session
- When the user changes the theme in the settings menu
For more information and to upvote an improvement, see GitHub issue
`#11920 <https://github.com/streamlit/streamlit/issues/11920>`_.
Attributes
----------
type : "light", "dark"
The theme type inferred from the background color of the app.
Example
-------
Access the theme type of the app:
>>> import streamlit as st
>>>
>>> st.write(f"The current theme type is {st.context.theme.type}.")
"""
ctx = get_script_run_ctx()
if ctx is None or ctx.context_info is None:
return StreamlitTheme({"type": None})
return StreamlitTheme.from_context_info({"type": ctx.context_info.color_scheme})
@property
@gather_metrics("context.timezone")
def timezone(self) -> str | None:
"""The read-only timezone of the user's browser.
Example
-------
Access the user's timezone, and format a datetime to display locally:
>>> import streamlit as st
>>> from datetime import datetime, timezone
>>> import pytz
>>>
>>> tz = st.context.timezone
>>> tz_obj = pytz.timezone(tz)
>>>
>>> now = datetime.now(timezone.utc)
>>>
>>> f"The user's timezone is {tz}."
>>> f"The UTC time is {now}."
>>> f"The user's local time is {now.astimezone(tz_obj)}"
"""
ctx = get_script_run_ctx()
if ctx is None or ctx.context_info is None:
return None
return ctx.context_info.timezone
@property
@gather_metrics("context.timezone_offset")
def timezone_offset(self) -> int | None:
"""The read-only timezone offset of the user's browser.
Example
-------
Access the user's timezone offset, and format a datetime to display locally:
>>> import streamlit as st
>>> from datetime import datetime, timezone, timedelta
>>>
>>> tzoff = st.context.timezone_offset
>>> tz_obj = timezone(-timedelta(minutes=tzoff))
>>>
>>> now = datetime.now(timezone.utc)
>>>
>>> f"The user's timezone is {tz}."
>>> f"The UTC time is {now}."
>>> f"The user's local time is {now.astimezone(tz_obj)}"
"""
ctx = get_script_run_ctx()
if ctx is None or ctx.context_info is None:
return None
return ctx.context_info.timezone_offset
@property
@gather_metrics("context.locale")
def locale(self) -> str | None:
"""The read-only locale of the user's browser.
``st.context.locale`` returns the value of |navigator.language|_ from
the user's DOM. This is a string representing the user's preferred
language (e.g. "en-US").
.. |navigator.language| replace:: ``navigator.language``
.. _navigator.language: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
Example
-------
Access the user's locale to display locally:
>>> import streamlit as st
>>>
>>> if st.context.locale == "fr-FR":
>>> st.write("Bonjour!")
>>> else:
>>> st.write("Hello!")
"""
ctx = get_script_run_ctx()
if ctx is None or ctx.context_info is None:
return None
return ctx.context_info.locale
@property
@gather_metrics("context.url")
def url(self) -> str | None:
"""The read-only URL of the app in the user's browser.
``st.context.url`` returns the URL through which the user is accessing
the app. This includes the scheme, domain name, port, and path. If
query parameters or anchors are present in the URL, they are removed
and not included in this value.
Example
-------
Conditionally show content when you access your app through
``localhost``:
>>> import streamlit as st
>>>
>>> if st.context.url.startswith("http://localhost"):
>>> st.write("You are running the app locally.")
"""
ctx = get_script_run_ctx()
if ctx is None or ctx.context_info is None:
return None
url_from_frontend = ctx.context_info.url
url_without_page_prefix = maybe_trim_page_path(
url_from_frontend, ctx.pages_manager
)
return maybe_add_page_path(url_without_page_prefix, ctx.pages_manager)
@property
@gather_metrics("context.ip_address")
def ip_address(self) -> str | None:
"""The read-only IP address of the user's connection.
This should not be used for security measures because it can easily be
spoofed. When a user accesses the app through ``localhost``, the IP
address is ``None``. Otherwise, the IP address is determined from the
|remote_ip|_ attribute of the Tornado request object and may be an
IPv4 or IPv6 address.
.. |remote_ip| replace:: ``remote_ip``
.. _remote_ip: https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest.remote_ip
Example
-------
Check if the user has an IPv4 or IPv6 address:
>>> import streamlit as st
>>>
>>> ip = st.context.ip_address
>>> if ip is None:
>>> st.write("No IP address. This is expected in local development.")
>>> elif ip.contains(":"):
>>> st.write("You have an IPv6 address.")
>>> elif ip.contains("."):
>>> st.write("You have an IPv4 address.")
>>> else:
>>> st.error("This should not happen.")
"""
session_client_request = _get_request()
if session_client_request is not None:
remote_ip = session_client_request.remote_ip
if remote_ip in {"::1", "127.0.0.1"}:
return None
return remote_ip
return None
@property
@gather_metrics("context.is_embedded")
def is_embedded(self) -> bool | None:
"""Whether the app is embedded.
This property returns a boolean value indicating whether the app is
running in an embedded context. This is determined by the presence of
``embed=true`` as a query parameter in the URL. This is the only way to
determine if the app is currently configured for embedding because
embedding settings are not accessible through ``st.query_params`` or
``st.context.url``.
Example
-------
Conditionally show content when the app is running in an embedded
context:
>>> import streamlit as st
>>>
>>> if st.context.is_embedded:
>>> st.write("You are running the app in an embedded context.")
"""
ctx = get_script_run_ctx()
if ctx is None or ctx.context_info is None:
return None
return ctx.context_info.is_embedded