first commit

This commit is contained in:
Ayxan
2022-05-23 00:16:32 +04:00
commit d660f2a4ca
24786 changed files with 4428337 additions and 0 deletions

View File

@ -0,0 +1,24 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
# Modules that the user should have access to. These are imported with "as"
# syntax pass mypy checking with implicit_reexport disabled.
from .components import declare_component as declare_component
# `html` and `iframe` are part of Custom Components, so they appear in this
# `streamlit.components.v1` namespace.
import streamlit
html = streamlit._main._html
iframe = streamlit._main._iframe

View File

@ -0,0 +1,380 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
"""
import pandas as pd
import pyarrow as pa
from streamlit import type_util, util
def marshall(proto, data, default_uuid=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 type_util.is_pandas_styler(data):
_marshall_styler(proto, data, default_uuid)
df = type_util.convert_anything_to_df(data)
_marshall_index(proto, df.index)
_marshall_columns(proto, df.columns)
_marshall_data(proto, df.to_numpy())
def _marshall_styler(proto, styler, default_uuid):
"""Marshall pandas.Styler styling data into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
styler : pandas.Styler
Styler holding styling data for the dataframe.
default_uuid : str
If Styler custom uuid is not provided, this value will be used.
"""
# NB: UUID should be set before _compute is called.
_marshall_uuid(proto, styler, default_uuid)
# NB: We're using protected members of Styler to get styles,
# which is non-ideal and could break if Styler's interface changes.
styler._compute()
# In Pandas 1.3.0, styler._translate() signature was changed.
# 2 arguments were added: sparse_index and sparse_columns.
# The functionality that they provide is not yet supported.
if type_util.is_pandas_version_less_than("1.3.0"):
pandas_styles = styler._translate()
else:
pandas_styles = styler._translate(False, False)
_marshall_caption(proto, styler)
_marshall_styles(proto, styler, pandas_styles)
_marshall_display_values(proto, styler.data, pandas_styles)
def _marshall_uuid(proto, styler, default_uuid):
"""Marshall pandas.Styler UUID into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
styler : pandas.Styler
Styler holding styling data for the dataframe.
default_uuid : str
If Styler custom 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, styler):
"""Marshall pandas.Styler caption into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
styler : pandas.Styler
Styler holding styling data for the dataframe.
"""
if styler.caption is not None:
proto.styler.caption = styler.caption
def _marshall_styles(proto, styler, styles):
"""Marshall pandas.Styler styles into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
styler : pandas.Styler
Styler holding styling data for the dataframe.
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:
# NB: styles in "table_styles" have a space
# between the UUID and the 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)
css_rules.append(rule)
if len(css_rules) > 0:
proto.styler.styles = "\n".join(css_rules)
def _trim_pandas_styles(styles):
"""Trim pandas styles dict.
Parameters
----------
styles : dict
pandas.Styler translated styles.
"""
# Filter out empty styles, as every cell will have a class
# but the list of props may just be [['', '']].
return [x for x in styles if any(any(y) for y in x["props"])]
def _pandas_style_to_css(style_type, style, uuid, separator=""):
"""Convert pandas.Styler translated styles entry to CSS.
Parameters
----------
style : dict
pandas.Styler translated styles entry.
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 = css_property.strip() + ": " + css_value.strip()
declarations.append(declaration)
table_selector = "#T_" + str(uuid)
# In pandas < 1.1.0
# translated_style["cellstyle"] has the following shape:
# [
# {
# "props": [["color", " black"], ["background-color", "orange"], ["", ""]],
# "selector": "row0_col0"
# }
# ...
# ]
#
# 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" or (
style_type == "cell_style" and type_util.is_pandas_version_less_than("1.1.0")
):
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, df, styles):
"""Marshall pandas.Styler display values into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable 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_to_pybytes(new_df)
def _use_display_values(df, styles):
"""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.
"""
# (HK) TODO: Rewrite this method without using regex.
import re
# If values in a column are not of the same type, Arrow Table
# 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:
cell_id = cell["id"]
match = cell_selector_regex.match(cell_id)
if match:
r, c = map(int, match.groups())
new_df.iat[r, c] = str(cell["display_value"])
return new_df
def _dataframe_to_pybytes(df):
"""Convert pandas.DataFrame to pybytes.
Parameters
----------
df : pandas.DataFrame
A dataframe to convert.
"""
table = pa.Table.from_pandas(df)
sink = pa.BufferOutputStream()
writer = pa.RecordBatchStreamWriter(sink, table.schema)
writer.write_table(table)
writer.close()
return sink.getvalue().to_pybytes()
def _marshall_index(proto, index):
"""Marshall pandas.DataFrame index into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
index : Index or array-like
Index to use for resulting frame.
Will default to RangeIndex (0, 1, 2, ..., n) if no index is provided.
"""
index = map(util._maybe_tuple_to_list, index.values)
index_df = pd.DataFrame(index)
proto.index = _dataframe_to_pybytes(index_df)
def _marshall_columns(proto, columns):
"""Marshall pandas.DataFrame columns into an ArrowTable proto.
Parameters
----------
proto : proto.ArrowTable
Output. The protobuf for a Streamlit ArrowTable proto.
columns : Index or array-like
Column labels to use for resulting frame.
Will default to RangeIndex (0, 1, 2, ..., n) if no column labels are provided.
"""
columns = map(util._maybe_tuple_to_list, columns.values)
columns_df = pd.DataFrame(columns)
proto.columns = _dataframe_to_pybytes(columns_df)
def _marshall_data(proto, data):
"""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.
"""
df = pd.DataFrame(data)
proto.data = _dataframe_to_pybytes(df)
def arrow_proto_to_dataframe(proto):
"""Convert ArrowTable proto to pandas.DataFrame.
Parameters
----------
proto : proto.ArrowTable
Output. pandas.DataFrame
"""
data = _pybytes_to_dataframe(proto.data)
index = _pybytes_to_dataframe(proto.index)
columns = _pybytes_to_dataframe(proto.columns)
return pd.DataFrame(
data.values, index=index.values.T.tolist(), columns=columns.values.T.tolist()
)
def _pybytes_to_dataframe(source):
"""Convert pybytes to pandas.DataFrame.
Parameters
----------
source : pybytes
Will default to RangeIndex (0, 1, 2, ..., n) if no `index` or `columns` are provided.
"""
reader = pa.RecordBatchStreamReader(source)
return reader.read_pandas()

View File

@ -0,0 +1,442 @@
# Copyright 2018-2022 Streamlit Inc.
#
# 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.
import inspect
import json
import mimetypes
import os
import threading
from typing import Any, Dict, Optional, Type, Union
import tornado.web
from streamlit.scriptrunner import get_script_run_ctx
import streamlit.server.routes
from streamlit import type_util
from streamlit.elements.form import current_form_id
from streamlit import util
from streamlit.errors import StreamlitAPIException
from streamlit.logger import get_logger
from streamlit.proto.Components_pb2 import SpecialArg, ArrowTable as ArrowTableProto
from streamlit.proto.Element_pb2 import Element
from streamlit.state import NoValue, register_widget
from streamlit.type_util import to_bytes
LOGGER = get_logger(__name__)
class MarshallComponentException(StreamlitAPIException):
"""Class for exceptions generated during custom component marshalling."""
pass
class CustomComponent:
"""A Custom Component declaration."""
def __init__(
self,
name: str,
path: Optional[str] = None,
url: Optional[str] = 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
def __repr__(self) -> str:
return util.repr_(self)
@property
def abspath(self) -> Optional[str]:
"""The absolute path that the component is served from."""
if self.path is None:
return None
return os.path.abspath(self.path)
def __call__(
self,
*args,
default: Any = None,
key: Optional[str] = None,
**kwargs,
) -> Any:
"""An alias for create_instance."""
return self.create_instance(*args, default=default, key=key, **kwargs)
def create_instance(
self,
*args,
default: Any = None,
key: Optional[str] = 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".
**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
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."""
)
# 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 type_util.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 type_util.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 BaseException as e:
raise MarshallComponentException(
"Could not convert component args to JSON", e
)
def marshall_component(dg, element: Element) -> Union[Any, Type[NoValue]]:
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 the entirety of its protobuf. 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.
#
# So: if `key` is None, we marshall the element's arguments
# *before* computing its widget_ui_value (which creates its hash).
# If `key` is not None, we marshall the arguments *after*.
def marshall_element_args():
element.component_instance.json_args = serialized_json_args
element.component_instance.special_args.extend(special_args)
if key is None:
marshall_element_args()
def deserialize_component(ui_value, widget_id=""):
# ui_value is an object from json, an ArrowTable proto, or a bytearray
return ui_value
ctx = get_script_run_ctx()
widget_value, _ = register_widget(
element_type="component_instance",
element_proto=element.component_instance,
user_key=key,
widget_func_name=self.name,
deserializer=deserialize_component,
serializer=lambda x: x,
ctx=ctx,
)
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)
# widget_value will be either None or whatever the component's most
# recent setWidgetValue value is. We coerce None -> NoValue,
# because that's what DeltaGenerator._enqueue expects.
return widget_value if widget_value is not None else NoValue
# 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 = streamlit._main
element = Element()
return_value = marshall_component(dg, element)
result = dg._enqueue(
"component_instance", element.component_instance, return_value
)
return result
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
)
def __ne__(self, other) -> bool:
"""Inequality operator."""
return not self == other
def __str__(self) -> str:
return f"'{self.name}': {self.path if self.path is not None else self.url}"
def declare_component(
name: str,
path: Optional[str] = None,
url: Optional[str] = None,
) -> CustomComponent:
"""Create and register a custom component.
Parameters
----------
name: str
A short, descriptive name for the component. Like, "slider".
path: str or None
The path to serve the component's frontend files from. Either
`path` or `url` must be specified, but not both.
url: str or None
The URL that the component is served from. 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.
"""
# Get our stack frame.
current_frame = 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
# 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)
# 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)
ComponentRegistry.instance().register_component(component)
return component
class ComponentRequestHandler(tornado.web.RequestHandler):
def initialize(self, registry: "ComponentRegistry"):
self._registry = registry
def get(self, path: str) -> None:
parts = path.split("/")
component_name = parts[0]
component_root = self._registry.get_component_path(component_name)
if component_root is None:
self.write("not found")
self.set_status(404)
return
filename = "/".join(parts[1:])
abspath = os.path.join(component_root, filename)
LOGGER.debug("ComponentRequestHandler: GET: %s -> %s", path, abspath)
try:
with open(abspath, "rb") as file:
contents = file.read()
except (OSError) as e:
LOGGER.error(f"ComponentRequestHandler: GET {path} read error", exc_info=e)
self.write("read error")
self.set_status(404)
return
self.write(contents)
self.set_header("Content-Type", self.get_content_type(abspath))
self.set_extra_headers(path)
def set_extra_headers(self, path) -> None:
"""Disable cache for HTML files.
Other assets like JS and CSS are suffixed with their hash, so they can
be cached indefinitely.
"""
is_index_url = len(path) == 0
if is_index_url or path.endswith(".html"):
self.set_header("Cache-Control", "no-cache")
else:
self.set_header("Cache-Control", "public")
def set_default_headers(self) -> None:
if streamlit.server.routes.allow_cross_origin_requests():
self.set_header("Access-Control-Allow-Origin", "*")
def options(self) -> None:
"""/OPTIONS handler for preflight CORS checks."""
self.set_status(204)
self.finish()
@staticmethod
def get_content_type(abspath) -> str:
"""Returns the ``Content-Type`` header to be used for this request.
From tornado.web.StaticFileHandler.
"""
mime_type, encoding = mimetypes.guess_type(abspath)
# per RFC 6713, use the appropriate type for a gzip compressed file
if encoding == "gzip":
return "application/gzip"
# As of 2015-07-21 there is no bzip2 encoding defined at
# http://www.iana.org/assignments/media-types/media-types.xhtml
# So for that (and any other encoding), use octet-stream.
elif encoding is not None:
return "application/octet-stream"
elif mime_type is not None:
return mime_type
# if mime_type not detected, use application/octet-stream
else:
return "application/octet-stream"
@staticmethod
def get_url(file_id: str) -> str:
"""Return the URL for a component file with the given ID."""
return "components/{}".format(file_id)
class ComponentRegistry:
_instance_lock = threading.Lock()
_instance = None # type: Optional[ComponentRegistry]
@classmethod
def instance(cls) -> "ComponentRegistry":
"""Returns the singleton ComponentRegistry"""
# We use a double-checked locking optimization to avoid the overhead
# of acquiring the lock in the common case:
# https://en.wikipedia.org/wiki/Double-checked_locking
if cls._instance is None:
with cls._instance_lock:
if cls._instance is None:
cls._instance = ComponentRegistry()
return cls._instance
def __init__(self):
self._components = {} # type: Dict[str, CustomComponent]
self._lock = threading.Lock()
def __repr__(self) -> str:
return util.repr_(self)
def register_component(self, component: CustomComponent) -> None:
"""Register a CustomComponent.
Parameters
----------
component : CustomComponent
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) -> Optional[str]:
"""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