streamlit 1.49.1__py3-none-any.whl → 1.51.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- streamlit/__init__.py +4 -1
- streamlit/column_config.py +2 -0
- streamlit/commands/navigation.py +7 -7
- streamlit/commands/page_config.py +4 -6
- streamlit/components/v1/custom_component.py +17 -42
- streamlit/components/v2/__init__.py +458 -0
- streamlit/components/v2/bidi_component/__init__.py +20 -0
- streamlit/components/v2/bidi_component/constants.py +29 -0
- streamlit/components/v2/bidi_component/main.py +386 -0
- streamlit/components/v2/bidi_component/serialization.py +265 -0
- streamlit/components/v2/bidi_component/state.py +92 -0
- streamlit/components/v2/component_definition_resolver.py +143 -0
- streamlit/components/v2/component_file_watcher.py +403 -0
- streamlit/components/v2/component_manager.py +431 -0
- streamlit/components/v2/component_manifest_handler.py +122 -0
- streamlit/components/v2/component_path_utils.py +245 -0
- streamlit/components/v2/component_registry.py +409 -0
- streamlit/components/v2/get_bidi_component_manager.py +51 -0
- streamlit/components/v2/manifest_scanner.py +615 -0
- streamlit/components/v2/presentation.py +198 -0
- streamlit/components/v2/types.py +324 -0
- streamlit/config.py +741 -32
- streamlit/config_option.py +4 -1
- streamlit/config_util.py +650 -1
- streamlit/connections/base_connection.py +4 -2
- streamlit/dataframe_util.py +18 -10
- streamlit/delta_generator.py +8 -7
- streamlit/delta_generator_singletons.py +3 -1
- streamlit/deprecation_util.py +17 -6
- streamlit/elements/arrow.py +90 -42
- streamlit/elements/deck_gl_json_chart.py +98 -39
- streamlit/elements/dialog_decorator.py +2 -1
- streamlit/elements/exception.py +3 -1
- streamlit/elements/form.py +6 -6
- streamlit/elements/graphviz_chart.py +24 -9
- streamlit/elements/heading.py +3 -5
- streamlit/elements/iframe.py +0 -2
- streamlit/elements/image.py +12 -13
- streamlit/elements/layouts.py +89 -22
- streamlit/elements/lib/built_in_chart_utils.py +95 -31
- streamlit/elements/lib/color_util.py +8 -18
- streamlit/elements/lib/column_config_utils.py +9 -8
- streamlit/elements/lib/column_types.py +595 -148
- streamlit/elements/lib/dialog.py +3 -2
- streamlit/elements/lib/image_utils.py +3 -5
- streamlit/elements/lib/layout_utils.py +50 -13
- streamlit/elements/lib/mutable_status_container.py +2 -2
- streamlit/elements/lib/options_selector_utils.py +2 -2
- streamlit/elements/lib/pandas_styler_utils.py +30 -14
- streamlit/elements/lib/utils.py +21 -9
- streamlit/elements/map.py +81 -40
- streamlit/elements/media.py +7 -7
- streamlit/elements/metric.py +11 -35
- streamlit/elements/pdf.py +2 -4
- streamlit/elements/plotly_chart.py +142 -26
- streamlit/elements/progress.py +2 -4
- streamlit/elements/pyplot.py +6 -6
- streamlit/elements/space.py +113 -0
- streamlit/elements/vega_charts.py +400 -143
- streamlit/elements/widgets/audio_input.py +52 -4
- streamlit/elements/widgets/button.py +29 -29
- streamlit/elements/widgets/button_group.py +33 -6
- streamlit/elements/widgets/camera_input.py +3 -4
- streamlit/elements/widgets/chat.py +7 -0
- streamlit/elements/widgets/checkbox.py +1 -0
- streamlit/elements/widgets/color_picker.py +1 -0
- streamlit/elements/widgets/data_editor.py +34 -29
- streamlit/elements/widgets/file_uploader.py +6 -10
- streamlit/elements/widgets/multiselect.py +14 -3
- streamlit/elements/widgets/number_input.py +5 -4
- streamlit/elements/widgets/radio.py +10 -2
- streamlit/elements/widgets/select_slider.py +8 -4
- streamlit/elements/widgets/selectbox.py +9 -2
- streamlit/elements/widgets/slider.py +38 -41
- streamlit/elements/widgets/text_widgets.py +6 -0
- streamlit/elements/widgets/time_widgets.py +15 -12
- streamlit/elements/write.py +28 -23
- streamlit/emojis.py +1 -1
- streamlit/errors.py +115 -0
- streamlit/git_util.py +65 -43
- streamlit/hello/hello.py +8 -0
- streamlit/hello/utils.py +2 -1
- streamlit/material_icon_names.py +1 -1
- streamlit/navigation/page.py +4 -1
- streamlit/proto/ArrowData_pb2.py +27 -0
- streamlit/proto/ArrowData_pb2.pyi +46 -0
- streamlit/proto/Arrow_pb2.py +10 -8
- streamlit/proto/Arrow_pb2.pyi +31 -2
- streamlit/proto/AudioInput_pb2.py +2 -2
- streamlit/proto/AudioInput_pb2.pyi +6 -2
- streamlit/proto/BidiComponent_pb2.py +34 -0
- streamlit/proto/BidiComponent_pb2.pyi +153 -0
- streamlit/proto/Block_pb2.py +11 -11
- streamlit/proto/Block_pb2.pyi +9 -1
- streamlit/proto/DeckGlJsonChart_pb2.py +10 -4
- streamlit/proto/DeckGlJsonChart_pb2.pyi +9 -3
- streamlit/proto/Element_pb2.py +5 -3
- streamlit/proto/Element_pb2.pyi +14 -4
- streamlit/proto/HeightConfig_pb2.py +2 -2
- streamlit/proto/HeightConfig_pb2.pyi +6 -3
- streamlit/proto/NewSession_pb2.py +18 -16
- streamlit/proto/NewSession_pb2.pyi +158 -6
- streamlit/proto/PlotlyChart_pb2.py +8 -6
- streamlit/proto/PlotlyChart_pb2.pyi +3 -1
- streamlit/proto/Space_pb2.py +27 -0
- streamlit/proto/Space_pb2.pyi +42 -0
- streamlit/proto/WidgetStates_pb2.py +2 -2
- streamlit/proto/WidgetStates_pb2.pyi +13 -3
- streamlit/proto/WidthConfig_pb2.py +2 -2
- streamlit/proto/WidthConfig_pb2.pyi +6 -3
- streamlit/runtime/app_session.py +45 -6
- streamlit/runtime/caching/cache_data_api.py +4 -4
- streamlit/runtime/caching/cache_errors.py +4 -1
- streamlit/runtime/caching/cache_resource_api.py +3 -2
- streamlit/runtime/caching/cache_utils.py +2 -1
- streamlit/runtime/caching/cached_message_replay.py +3 -3
- streamlit/runtime/caching/hashing.py +3 -4
- streamlit/runtime/caching/legacy_cache_api.py +2 -1
- streamlit/runtime/connection_factory.py +1 -3
- streamlit/runtime/forward_msg_queue.py +4 -1
- streamlit/runtime/fragment.py +2 -1
- streamlit/runtime/memory_media_file_storage.py +1 -1
- streamlit/runtime/metrics_util.py +6 -2
- streamlit/runtime/runtime.py +14 -0
- streamlit/runtime/scriptrunner/exec_code.py +2 -1
- streamlit/runtime/scriptrunner/script_runner.py +2 -2
- streamlit/runtime/scriptrunner_utils/script_run_context.py +3 -6
- streamlit/runtime/secrets.py +2 -4
- streamlit/runtime/session_manager.py +3 -1
- streamlit/runtime/state/common.py +30 -5
- streamlit/runtime/state/presentation.py +85 -0
- streamlit/runtime/state/safe_session_state.py +2 -2
- streamlit/runtime/state/session_state.py +220 -16
- streamlit/runtime/state/widgets.py +19 -3
- streamlit/runtime/theme_util.py +148 -0
- streamlit/runtime/websocket_session_manager.py +3 -1
- streamlit/source_util.py +2 -2
- streamlit/static/index.html +2 -2
- streamlit/static/manifest.json +244 -227
- streamlit/static/static/css/{index.C8X8rNzw.css → index.BpABIXK9.css} +1 -1
- streamlit/static/static/css/index.DgR7E2CV.css +1 -0
- streamlit/static/static/js/{ErrorOutline.esm.DcGrhbBP.js → ErrorOutline.esm.YoJdlW1p.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.DgBvV6Pq.js → FileDownload.esm.Ddx8VEYy.js} +1 -1
- streamlit/static/static/js/{FileHelper.M6AAaeuA.js → FileHelper.90EtOmj9.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.DHh1GFzm.js → FormClearHelper.BB1Km6eP.js} +1 -1
- streamlit/static/static/js/InputInstructions.jhH15PqV.js +1 -0
- streamlit/static/static/js/{Particles.DDVT-6Qc.js → Particles.DUsputn1.js} +1 -1
- streamlit/static/static/js/{ProgressBar.BEY0cXXV.js → ProgressBar.DLY8H6nE.js} +2 -2
- streamlit/static/static/js/Toolbar.D8nHCkuz.js +1 -0
- streamlit/static/static/js/{base-input.CK3UVGp1.js → base-input.CJGiNqed.js} +3 -3
- streamlit/static/static/js/{checkbox.D8W881TL.js → checkbox.Cpdd482O.js} +1 -1
- streamlit/static/static/js/{createSuper.B6W-Dh9S.js → createSuper.CuQIogbW.js} +1 -1
- streamlit/static/static/js/data-grid-overlay-editor.2Ufgxc6y.js +1 -0
- streamlit/static/static/js/{downloader.DiKpuU_S.js → downloader.CN0K7xlu.js} +1 -1
- streamlit/static/static/js/{es6.B8zRNPZ-.js → es6.BJcsVXQ0.js} +2 -2
- streamlit/static/static/js/{iframeResizer.contentWindow.DIewJmmh.js → iframeResizer.contentWindow.XzUvQqcZ.js} +1 -1
- streamlit/static/static/js/index.B1ZQh4P1.js +1 -0
- streamlit/static/static/js/index.BKstZk0M.js +27 -0
- streamlit/static/static/js/{index.Bte_9Lyq.js → index.BMcFsUee.js} +1 -1
- streamlit/static/static/js/{index.qhs54UAB.js → index.BR-IdcTb.js} +1 -1
- streamlit/static/static/js/{index.CejBxbg1.js → index.B_dWA3vd.js} +1 -1
- streamlit/static/static/js/{index.D5naqx-J.js → index.BgnZEMVh.js} +1 -1
- streamlit/static/static/js/{index.C7fRKRs4.js → index.BohqXifI.js} +1 -1
- streamlit/static/static/js/{index.cnnXF7xQ.js → index.Br5nxKNj.js} +1 -1
- streamlit/static/static/js/index.BrIKVbNc.js +3 -0
- streamlit/static/static/js/index.BtWUPzle.js +1 -0
- streamlit/static/static/js/index.C0RLraek.js +1 -0
- streamlit/static/static/js/{index.CP5TD2z1.js → index.CAIjskgG.js} +1 -1
- streamlit/static/static/js/{index.CD8HuT3N.js → index.CAj-7vWz.js} +135 -162
- streamlit/static/static/js/{index.DtYN2x4k.js → index.CMtEit2O.js} +1 -1
- streamlit/static/static/js/index.CkRlykEE.js +12 -0
- streamlit/static/static/js/{index.Ts_0SdB9.js → index.CmN3FXfI.js} +2 -2
- streamlit/static/static/js/{index.BnEpvLEz.js → index.CwbFI1_-.js} +1 -1
- streamlit/static/static/js/{index.CcJf6BCU.js → index.CxIUUfab.js} +27 -27
- streamlit/static/static/js/index.D2KPNy7e.js +1 -0
- streamlit/static/static/js/{index.Ch7MBCx0.js → index.D3GPA5k4.js} +47 -47
- streamlit/static/static/js/{index.ho6NIXGl.js → index.DGAh7DMq.js} +1 -1
- streamlit/static/static/js/index.DKb_NvmG.js +197 -0
- streamlit/static/static/js/{index.CvYYtxD_.js → index.DMqgUYKq.js} +1 -1
- streamlit/static/static/js/{index.zecpGxtj.js → index.DOFlg3dS.js} +1 -1
- streamlit/static/static/js/{index.B9mjBcgE.js → index.DPUXkcQL.js} +1 -1
- streamlit/static/static/js/index.DX1xY89g.js +1 -0
- streamlit/static/static/js/index.DYATBCsq.js +2 -0
- streamlit/static/static/js/{index.D2-atlaQ.js → index.DaSmGJ76.js} +3 -3
- streamlit/static/static/js/index.Dd7bMeLP.js +1 -0
- streamlit/static/static/js/{index.4eF4NxG2.js → index.DjmmgI5U.js} +1 -1
- streamlit/static/static/js/index.Dq56CyM2.js +1 -0
- streamlit/static/static/js/index.DuiXaS5_.js +7 -0
- streamlit/static/static/js/index.DvFidMLe.js +2 -0
- streamlit/static/static/js/{index.452cqrrL.js → index.DwkhC5Pc.js} +1 -1
- streamlit/static/static/js/{index.Dk4C7X3i.js → index.Q-3sFn1v.js} +1 -1
- streamlit/static/static/js/{index.CjXWwH-y.js → index.QJ5QO9sJ.js} +1 -1
- streamlit/static/static/js/{index.B6U8LQo3.js → index.VwTaeety.js} +1 -1
- streamlit/static/static/js/index.YOqQbeX8.js +1 -0
- streamlit/static/static/js/{input.nzVJphXi.js → input.D4MN_FzN.js} +1 -1
- streamlit/static/static/js/{memory.CjCgTQz3.js → memory.DrZjtdGT.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.DaRFzZEO.js → number-overlay-editor.DRwAw1In.js} +1 -1
- streamlit/static/static/js/{possibleConstructorReturn.DgiPnZ9N.js → possibleConstructorReturn.exeeJQEP.js} +1 -1
- streamlit/static/static/js/record.B-tDciZb.js +1 -0
- streamlit/static/static/js/{sandbox.mithfq7Z.js → sandbox.ClO3IuUr.js} +1 -1
- streamlit/static/static/js/{timepicker.Dbl5KFh6.js → timepicker.DAhu-vcF.js} +4 -4
- streamlit/static/static/js/{toConsumableArray.D-Dx88BQ.js → toConsumableArray.DNbljYEC.js} +1 -1
- streamlit/static/static/js/{uniqueId.Bh26R_3S.js → uniqueId.oG4Gvj1v.js} +1 -1
- streamlit/static/static/js/{useBasicWidgetState.DeK-QJpD.js → useBasicWidgetState.D6sOH6oI.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.4iAdLWD-.js → useTextInputAutoExpand.4u3_GcuN.js} +2 -2
- streamlit/static/static/js/{useUpdateUiValue.CmT7_nJN.js → useUpdateUiValue.F2R3eTeR.js} +1 -1
- streamlit/static/static/js/wavesurfer.esm.vI8Eid4k.js +73 -0
- streamlit/static/static/js/withFullScreenWrapper.zothJIsI.js +1 -0
- streamlit/static/static/media/MaterialSymbols-Rounded.C7IFxh57.woff2 +0 -0
- streamlit/string_util.py +56 -1
- streamlit/testing/v1/app_test.py +2 -2
- streamlit/testing/v1/element_tree.py +23 -9
- streamlit/testing/v1/util.py +2 -2
- streamlit/type_util.py +3 -4
- streamlit/url_util.py +1 -3
- streamlit/user_info.py +1 -2
- streamlit/util.py +3 -1
- streamlit/watcher/event_based_path_watcher.py +23 -12
- streamlit/watcher/local_sources_watcher.py +11 -1
- streamlit/watcher/path_watcher.py +9 -6
- streamlit/watcher/polling_path_watcher.py +4 -1
- streamlit/watcher/util.py +2 -2
- streamlit/web/bootstrap.py +0 -31
- streamlit/web/cli.py +51 -22
- streamlit/web/server/bidi_component_request_handler.py +193 -0
- streamlit/web/server/component_file_utils.py +97 -0
- streamlit/web/server/component_request_handler.py +8 -21
- streamlit/web/server/oidc_mixin.py +3 -1
- streamlit/web/server/routes.py +18 -5
- streamlit/web/server/server.py +10 -0
- streamlit/web/server/server_util.py +3 -1
- streamlit/web/server/upload_file_request_handler.py +3 -1
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/METADATA +4 -5
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/RECORD +238 -209
- streamlit/static/static/css/index.COe1010n.css +0 -1
- streamlit/static/static/js/Hooks.DGu1od_L.js +0 -1
- streamlit/static/static/js/InputInstructions.z6sVgyYt.js +0 -1
- streamlit/static/static/js/Toolbar.DSnK1fUh.js +0 -1
- streamlit/static/static/js/data-grid-overlay-editor.DRTHOydk.js +0 -1
- streamlit/static/static/js/index.BXYmrqnf.js +0 -1
- streamlit/static/static/js/index.B_8AnktO.js +0 -1
- streamlit/static/static/js/index.Bl7zGQSh.js +0 -7
- streamlit/static/static/js/index.BnJIOYn9.js +0 -73
- streamlit/static/static/js/index.C1HcTl5K.js +0 -1
- streamlit/static/static/js/index.C7lSmSOP.js +0 -1
- streamlit/static/static/js/index.C_tmcx4B.js +0 -1
- streamlit/static/static/js/index.D3K5nOu9.js +0 -197
- streamlit/static/static/js/index.DkKT3LUI.js +0 -1
- streamlit/static/static/js/index.MTPPBDHk.js +0 -2
- streamlit/static/static/js/index.pqW9AMJD.js +0 -3
- streamlit/static/static/js/index.urHgTgMQ.js +0 -12
- streamlit/static/static/js/index.wzkv_11M.js +0 -1
- streamlit/static/static/js/index.yF5AncHY.js +0 -1
- streamlit/static/static/js/withFullScreenWrapper.DLp1ENGm.js +0 -1
- streamlit/static/static/media/MaterialSymbols-Rounded.CBxVaFdk.woff2 +0 -0
- {streamlit-1.49.1.data → streamlit-1.51.0.data}/scripts/streamlit.cmd +0 -0
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/WHEEL +0 -0
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/entry_points.txt +0 -0
- {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Final
|
|
18
|
+
|
|
19
|
+
INTERNAL_COMPONENT_NAME: Final[str] = "bidi_component"
|
|
20
|
+
|
|
21
|
+
# Shared constant that delimits the base widget id from the event suffix.
|
|
22
|
+
# This value **must** stay in sync with its TypeScript counterpart defined in
|
|
23
|
+
# `frontend/lib/src/components/widgets/BidiComponent/constants.ts`.
|
|
24
|
+
EVENT_DELIM: Final[str] = "__"
|
|
25
|
+
|
|
26
|
+
# Shared constant that is used to identify ArrowReference objects in the data structure.
|
|
27
|
+
# This value **must** stay in sync with its TypeScript counterpart defined in
|
|
28
|
+
# `frontend/lib/src/components/widgets/BidiComponent/constants.ts`.
|
|
29
|
+
ARROW_REF_KEY: Final[str] = "__streamlit_arrow_ref__"
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
20
|
+
|
|
21
|
+
from streamlit.components.v2.bidi_component.constants import (
|
|
22
|
+
EVENT_DELIM,
|
|
23
|
+
INTERNAL_COMPONENT_NAME,
|
|
24
|
+
)
|
|
25
|
+
from streamlit.components.v2.bidi_component.serialization import (
|
|
26
|
+
BidiComponentSerde,
|
|
27
|
+
deserialize_trigger_list,
|
|
28
|
+
serialize_mixed_data,
|
|
29
|
+
)
|
|
30
|
+
from streamlit.components.v2.bidi_component.state import (
|
|
31
|
+
BidiComponentResult,
|
|
32
|
+
unwrap_component_state,
|
|
33
|
+
)
|
|
34
|
+
from streamlit.components.v2.presentation import make_bidi_component_presenter
|
|
35
|
+
from streamlit.dataframe_util import (
|
|
36
|
+
DataFormat,
|
|
37
|
+
convert_anything_to_arrow_bytes,
|
|
38
|
+
determine_data_format,
|
|
39
|
+
)
|
|
40
|
+
from streamlit.elements.lib.form_utils import current_form_id
|
|
41
|
+
from streamlit.elements.lib.layout_utils import (
|
|
42
|
+
Height,
|
|
43
|
+
LayoutConfig,
|
|
44
|
+
Width,
|
|
45
|
+
validate_width,
|
|
46
|
+
)
|
|
47
|
+
from streamlit.elements.lib.policies import check_cache_replay_rules
|
|
48
|
+
from streamlit.elements.lib.utils import compute_and_register_element_id, to_key
|
|
49
|
+
from streamlit.errors import (
|
|
50
|
+
BidiComponentInvalidCallbackNameError,
|
|
51
|
+
BidiComponentInvalidDefaultKeyError,
|
|
52
|
+
BidiComponentInvalidIdError,
|
|
53
|
+
BidiComponentMissingContentError,
|
|
54
|
+
BidiComponentUnserializableDataError,
|
|
55
|
+
)
|
|
56
|
+
from streamlit.proto.ArrowData_pb2 import ArrowData as ArrowDataProto
|
|
57
|
+
from streamlit.proto.BidiComponent_pb2 import BidiComponent as BidiComponentProto
|
|
58
|
+
from streamlit.runtime.metrics_util import gather_metrics
|
|
59
|
+
from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx
|
|
60
|
+
from streamlit.runtime.state import register_widget
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from streamlit.components.v2.types import (
|
|
64
|
+
BidiComponentData,
|
|
65
|
+
BidiComponentDefaults,
|
|
66
|
+
BidiComponentKey,
|
|
67
|
+
ComponentIsolateStyles,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if TYPE_CHECKING:
|
|
71
|
+
# Define DeltaGenerator for type checking the dg property
|
|
72
|
+
from streamlit.delta_generator import DeltaGenerator
|
|
73
|
+
from streamlit.runtime.state.common import WidgetCallback
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_trigger_id(base: str, event: str) -> str:
|
|
77
|
+
"""Construct the per-event *trigger widget* identifier.
|
|
78
|
+
|
|
79
|
+
The widget ID for a trigger is derived from the *base* component ID plus
|
|
80
|
+
an *event* name. We join those two parts with :py:const:`EVENT_DELIM` and
|
|
81
|
+
perform a couple of validations so that downstream logic can always split
|
|
82
|
+
the identifier unambiguously.
|
|
83
|
+
|
|
84
|
+
Trigger widgets are marked as internal by prefixing with an internal key prefix,
|
|
85
|
+
so they won't be exposed in `st.session_state` to end users.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
base
|
|
90
|
+
The unique, framework-assigned ID of the component instance.
|
|
91
|
+
event
|
|
92
|
+
The event name as provided by either the frontend or the developer
|
|
93
|
+
(e.g., "click", "change").
|
|
94
|
+
|
|
95
|
+
Returns
|
|
96
|
+
-------
|
|
97
|
+
str
|
|
98
|
+
The composite widget ID in the form ``"$$STREAMLIT_INTERNAL_KEY_{base}__{event}"``
|
|
99
|
+
where ``__`` is the delimiter.
|
|
100
|
+
|
|
101
|
+
Raises
|
|
102
|
+
------
|
|
103
|
+
StreamlitAPIException
|
|
104
|
+
If either `base` or `event` already contains the delimiter sequence.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
from streamlit.runtime.state.session_state import STREAMLIT_INTERNAL_KEY_PREFIX
|
|
108
|
+
|
|
109
|
+
if EVENT_DELIM in base:
|
|
110
|
+
raise BidiComponentInvalidIdError("base", EVENT_DELIM)
|
|
111
|
+
if EVENT_DELIM in event:
|
|
112
|
+
raise BidiComponentInvalidIdError("event", EVENT_DELIM)
|
|
113
|
+
|
|
114
|
+
return f"{STREAMLIT_INTERNAL_KEY_PREFIX}_{base}{EVENT_DELIM}{event}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BidiComponentMixin:
|
|
118
|
+
"""Mixin class for the bidi_component DeltaGenerator method."""
|
|
119
|
+
|
|
120
|
+
@gather_metrics("_bidi_component")
|
|
121
|
+
def _bidi_component(
|
|
122
|
+
self,
|
|
123
|
+
component_name: str,
|
|
124
|
+
key: BidiComponentKey = None,
|
|
125
|
+
isolate_styles: ComponentIsolateStyles = True,
|
|
126
|
+
data: BidiComponentData = None,
|
|
127
|
+
default: BidiComponentDefaults = None,
|
|
128
|
+
width: Width = "stretch",
|
|
129
|
+
height: Height = "content",
|
|
130
|
+
**kwargs: WidgetCallback | None,
|
|
131
|
+
) -> BidiComponentResult:
|
|
132
|
+
"""Add a bidirectional component instance to the app.
|
|
133
|
+
|
|
134
|
+
This method uses a component that has already been registered with the
|
|
135
|
+
application.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
component_name
|
|
140
|
+
The name of the registered component to use. The component's HTML,
|
|
141
|
+
CSS, and JavaScript will be loaded from the registry.
|
|
142
|
+
key
|
|
143
|
+
An optional string to use as the unique key for the component.
|
|
144
|
+
If this is omitted, a key will be generated based on the
|
|
145
|
+
component's execution sequence.
|
|
146
|
+
isolate_styles
|
|
147
|
+
Whether to sandbox the component's styles in a shadow root.
|
|
148
|
+
Defaults to True.
|
|
149
|
+
data
|
|
150
|
+
Data to pass to the component. This can be any JSON-serializable
|
|
151
|
+
data, or a pandas DataFrame, NumPy array, or other dataframe-like
|
|
152
|
+
object that can be serialized to Arrow.
|
|
153
|
+
default
|
|
154
|
+
A dictionary of default values for the component's state properties.
|
|
155
|
+
These defaults are applied only when the state key doesn't exist
|
|
156
|
+
in session state. Keys must correspond to valid state names (those
|
|
157
|
+
with `on_*_change` callbacks). Trigger values do not support
|
|
158
|
+
defaults.
|
|
159
|
+
width
|
|
160
|
+
The desired width of the component. This can be one of "stretch",
|
|
161
|
+
"content", or a number of pixels.
|
|
162
|
+
height
|
|
163
|
+
The desired height of the component. This can be one of "stretch",
|
|
164
|
+
"content", or a number of pixels.
|
|
165
|
+
**kwargs
|
|
166
|
+
Keyword arguments to pass to the component. Callbacks can be passed
|
|
167
|
+
here, with the naming convention `on_{event_name}_change`.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
BidiComponentResult
|
|
172
|
+
A dictionary-like object that holds the component's state and
|
|
173
|
+
trigger values.
|
|
174
|
+
|
|
175
|
+
Raises
|
|
176
|
+
------
|
|
177
|
+
ValueError
|
|
178
|
+
If the component name is not found in the registry.
|
|
179
|
+
StreamlitAPIException
|
|
180
|
+
If the component does not have the required JavaScript or HTML
|
|
181
|
+
content, or if the provided data cannot be serialized.
|
|
182
|
+
|
|
183
|
+
"""
|
|
184
|
+
check_cache_replay_rules()
|
|
185
|
+
|
|
186
|
+
key = to_key(key)
|
|
187
|
+
ctx = get_script_run_ctx()
|
|
188
|
+
|
|
189
|
+
if ctx is None:
|
|
190
|
+
# Create an empty state with the default value and return it
|
|
191
|
+
return BidiComponentResult({}, {})
|
|
192
|
+
|
|
193
|
+
# Get the component definition from the registry
|
|
194
|
+
from streamlit.runtime import Runtime
|
|
195
|
+
|
|
196
|
+
registry = Runtime.instance().bidi_component_registry
|
|
197
|
+
component_def = registry.get(component_name)
|
|
198
|
+
|
|
199
|
+
if component_def is None:
|
|
200
|
+
raise ValueError(f"Component '{component_name}' is not registered")
|
|
201
|
+
|
|
202
|
+
# Validate that the component has the required content
|
|
203
|
+
has_js = bool(component_def.js_content or component_def.js_url)
|
|
204
|
+
has_html = bool(component_def.html_content)
|
|
205
|
+
|
|
206
|
+
if not has_js and not has_html:
|
|
207
|
+
raise BidiComponentMissingContentError(component_name)
|
|
208
|
+
|
|
209
|
+
# Compute a unique ID for this component instance
|
|
210
|
+
computed_id = compute_and_register_element_id(
|
|
211
|
+
"bidi_component",
|
|
212
|
+
user_key=key,
|
|
213
|
+
component_name=component_name,
|
|
214
|
+
isolate_styles=isolate_styles,
|
|
215
|
+
width=width,
|
|
216
|
+
height=height,
|
|
217
|
+
dg=self.dg,
|
|
218
|
+
key_as_main_identity=True,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# ------------------------------------------------------------------
|
|
222
|
+
# 1. Parse user-supplied callbacks
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Event-specific callbacks follow the pattern ``on_<event>_change``.
|
|
225
|
+
# We deliberately *do not* support the legacy generic ``on_change``
|
|
226
|
+
# or ``on_<event>`` forms.
|
|
227
|
+
callbacks_by_event: dict[str, WidgetCallback] = {}
|
|
228
|
+
for kwarg_key, kwarg_value in list(kwargs.items()):
|
|
229
|
+
if not callable(kwarg_value):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if kwarg_key.startswith("on_") and kwarg_key.endswith("_change"):
|
|
233
|
+
# Preferred pattern: on_<event>_change
|
|
234
|
+
event_name = kwarg_key[3:-7] # strip prefix + suffix
|
|
235
|
+
else:
|
|
236
|
+
# Not an event callback we recognize - skip.
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
if not event_name or event_name == "_":
|
|
240
|
+
raise BidiComponentInvalidCallbackNameError(kwarg_key)
|
|
241
|
+
|
|
242
|
+
callbacks_by_event[event_name] = kwarg_value
|
|
243
|
+
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
# 2. Validate default keys against registered callbacks
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
if default is not None:
|
|
248
|
+
for state_key in default:
|
|
249
|
+
if state_key not in callbacks_by_event:
|
|
250
|
+
raise BidiComponentInvalidDefaultKeyError(
|
|
251
|
+
state_key, list(callbacks_by_event.keys())
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Set up the component proto
|
|
255
|
+
bidi_component_proto = BidiComponentProto()
|
|
256
|
+
bidi_component_proto.id = computed_id
|
|
257
|
+
bidi_component_proto.component_name = component_name
|
|
258
|
+
bidi_component_proto.isolate_styles = isolate_styles
|
|
259
|
+
bidi_component_proto.js_content = component_def.js_content or ""
|
|
260
|
+
bidi_component_proto.js_source_path = component_def.js_url or ""
|
|
261
|
+
bidi_component_proto.html_content = component_def.html_content or ""
|
|
262
|
+
bidi_component_proto.css_content = component_def.css_content or ""
|
|
263
|
+
bidi_component_proto.css_source_path = component_def.css_url or ""
|
|
264
|
+
|
|
265
|
+
validate_width(width, allow_content=True)
|
|
266
|
+
layout_config = LayoutConfig(width=width, height=height)
|
|
267
|
+
|
|
268
|
+
if data is not None:
|
|
269
|
+
try:
|
|
270
|
+
# 1. Raw byte payloads - forward as-is.
|
|
271
|
+
if isinstance(data, (bytes, bytearray)):
|
|
272
|
+
bidi_component_proto.bytes = bytes(data)
|
|
273
|
+
|
|
274
|
+
# 2. Mapping-like structures (e.g. plain dict) - check for mixed data.
|
|
275
|
+
elif isinstance(data, (Mapping, list, tuple)):
|
|
276
|
+
serialize_mixed_data(data, bidi_component_proto)
|
|
277
|
+
|
|
278
|
+
# 3. Dataframe-like structures - attempt Arrow serialization.
|
|
279
|
+
else:
|
|
280
|
+
data_format = determine_data_format(data)
|
|
281
|
+
|
|
282
|
+
if data_format != DataFormat.UNKNOWN:
|
|
283
|
+
arrow_bytes = convert_anything_to_arrow_bytes(data)
|
|
284
|
+
|
|
285
|
+
arrow_data_proto = ArrowDataProto()
|
|
286
|
+
arrow_data_proto.data = arrow_bytes
|
|
287
|
+
|
|
288
|
+
bidi_component_proto.arrow_data.CopyFrom(arrow_data_proto)
|
|
289
|
+
else:
|
|
290
|
+
# Fallback to JSON.
|
|
291
|
+
bidi_component_proto.json = json.dumps(data)
|
|
292
|
+
except Exception:
|
|
293
|
+
# As a last resort attempt JSON serialization so that we don't
|
|
294
|
+
# silently drop developer data.
|
|
295
|
+
try:
|
|
296
|
+
bidi_component_proto.json = json.dumps(data)
|
|
297
|
+
except Exception:
|
|
298
|
+
raise BidiComponentUnserializableDataError()
|
|
299
|
+
bidi_component_proto.form_id = current_form_id(self.dg)
|
|
300
|
+
|
|
301
|
+
# Instantiate the Serde for this component instance
|
|
302
|
+
serde = BidiComponentSerde(default=default)
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# 3. Prepare IDs and register widgets
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
# Compute trigger aggregator id from the base id
|
|
309
|
+
def _make_trigger_aggregator_id(base: str) -> str:
|
|
310
|
+
return _make_trigger_id(base, "events")
|
|
311
|
+
|
|
312
|
+
aggregator_id = _make_trigger_aggregator_id(computed_id)
|
|
313
|
+
|
|
314
|
+
# With generalized runtime dispatch, we can attach per-key callbacks
|
|
315
|
+
# directly to the state widget by passing the callbacks mapping.
|
|
316
|
+
# We also register a presenter to shape the user-visible session_state.
|
|
317
|
+
# Allowed state keys are the ones that have callbacks registered.
|
|
318
|
+
allowed_state_keys = (
|
|
319
|
+
set(callbacks_by_event.keys()) if callbacks_by_event else None
|
|
320
|
+
)
|
|
321
|
+
presenter = make_bidi_component_presenter(
|
|
322
|
+
aggregator_id,
|
|
323
|
+
computed_id,
|
|
324
|
+
allowed_state_keys,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
component_state = register_widget(
|
|
328
|
+
bidi_component_proto.id,
|
|
329
|
+
deserializer=serde.deserialize,
|
|
330
|
+
serializer=serde.serialize,
|
|
331
|
+
ctx=ctx,
|
|
332
|
+
callbacks=callbacks_by_event if callbacks_by_event else None,
|
|
333
|
+
value_type="json_value",
|
|
334
|
+
presenter=presenter,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# ------------------------------------------------------------------
|
|
338
|
+
# 4. Register a single *trigger aggregator* widget
|
|
339
|
+
# ------------------------------------------------------------------
|
|
340
|
+
trigger_vals: dict[str, Any] = {}
|
|
341
|
+
|
|
342
|
+
trig_state = register_widget(
|
|
343
|
+
aggregator_id,
|
|
344
|
+
deserializer=deserialize_trigger_list, # always returns list or None
|
|
345
|
+
serializer=lambda v: json.dumps(v), # send dict as JSON
|
|
346
|
+
ctx=ctx,
|
|
347
|
+
callbacks=callbacks_by_event if callbacks_by_event else None,
|
|
348
|
+
value_type="json_trigger_value",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Surface per-event trigger values derived from the aggregator payload list.
|
|
352
|
+
payloads: list[object] = trig_state.value or []
|
|
353
|
+
|
|
354
|
+
event_to_value: dict[str, Any] = {}
|
|
355
|
+
for payload in payloads:
|
|
356
|
+
if isinstance(payload, dict):
|
|
357
|
+
ev = payload.get("event")
|
|
358
|
+
if isinstance(ev, str):
|
|
359
|
+
event_to_value[ev] = payload.get("value")
|
|
360
|
+
|
|
361
|
+
for evt_name in callbacks_by_event:
|
|
362
|
+
trigger_vals[evt_name] = event_to_value.get(evt_name)
|
|
363
|
+
|
|
364
|
+
# Note: We intentionally do not inspect SessionState for additional
|
|
365
|
+
# trigger widget IDs here because doing so can raise KeyErrors when
|
|
366
|
+
# widgets are freshly registered but their values haven't been
|
|
367
|
+
# populated yet. Only the triggers explicitly registered above are
|
|
368
|
+
# included in the result object.
|
|
369
|
+
|
|
370
|
+
# ------------------------------------------------------------------
|
|
371
|
+
# 5. Enqueue proto and assemble the result object
|
|
372
|
+
# ------------------------------------------------------------------
|
|
373
|
+
self.dg._enqueue(
|
|
374
|
+
INTERNAL_COMPONENT_NAME,
|
|
375
|
+
bidi_component_proto,
|
|
376
|
+
layout_config=layout_config,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
state_vals = unwrap_component_state(component_state.value)
|
|
380
|
+
|
|
381
|
+
return BidiComponentResult(state_vals, trigger_vals)
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def dg(self) -> DeltaGenerator:
|
|
385
|
+
"""Get our DeltaGenerator."""
|
|
386
|
+
return cast("DeltaGenerator", self)
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
20
|
+
|
|
21
|
+
from streamlit.components.v2.bidi_component.constants import ARROW_REF_KEY
|
|
22
|
+
from streamlit.dataframe_util import convert_anything_to_arrow_bytes, is_dataframe_like
|
|
23
|
+
from streamlit.logger import get_logger
|
|
24
|
+
from streamlit.proto.BidiComponent_pb2 import BidiComponent as BidiComponentProto
|
|
25
|
+
from streamlit.proto.BidiComponent_pb2 import MixedData as MixedDataProto
|
|
26
|
+
from streamlit.util import AttributeDictionary
|
|
27
|
+
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from streamlit.components.v2.bidi_component.state import BidiComponentState
|
|
30
|
+
|
|
31
|
+
_LOGGER = get_logger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_dataframes_from_dict(
|
|
35
|
+
data: dict[str, Any], arrow_blobs: dict[str, bytes] | None = None
|
|
36
|
+
) -> dict[str, Any]:
|
|
37
|
+
"""Extract dataframe-like objects from a dictionary and replace them with
|
|
38
|
+
placeholders.
|
|
39
|
+
|
|
40
|
+
This function traverses the first level of a dictionary, detects any
|
|
41
|
+
dataframe-like objects, stores their Arrow bytes representation in the
|
|
42
|
+
`arrow_blobs` dictionary, and replaces them with JSON-serializable
|
|
43
|
+
placeholder objects.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
data
|
|
48
|
+
The dictionary to process. Only the first level is checked for
|
|
49
|
+
dataframe-like objects.
|
|
50
|
+
arrow_blobs
|
|
51
|
+
The dictionary to store the extracted Arrow bytes in, keyed by a unique
|
|
52
|
+
reference ID.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
dict[str, Any]
|
|
57
|
+
A new dictionary with dataframe-like objects replaced by placeholders.
|
|
58
|
+
"""
|
|
59
|
+
import uuid
|
|
60
|
+
|
|
61
|
+
if arrow_blobs is None:
|
|
62
|
+
arrow_blobs = {}
|
|
63
|
+
|
|
64
|
+
processed_data = {}
|
|
65
|
+
|
|
66
|
+
for key, value in data.items():
|
|
67
|
+
if is_dataframe_like(value):
|
|
68
|
+
# This is a dataframe-like object, serialize it to Arrow
|
|
69
|
+
try:
|
|
70
|
+
arrow_bytes = convert_anything_to_arrow_bytes(value)
|
|
71
|
+
ref_id = str(uuid.uuid4())
|
|
72
|
+
arrow_blobs[ref_id] = arrow_bytes
|
|
73
|
+
processed_data[key] = {ARROW_REF_KEY: ref_id}
|
|
74
|
+
except Exception:
|
|
75
|
+
# If Arrow serialization fails, keep the original value for JSON serialization
|
|
76
|
+
processed_data[key] = value
|
|
77
|
+
else:
|
|
78
|
+
# Not dataframe-like, keep as-is
|
|
79
|
+
processed_data[key] = value
|
|
80
|
+
|
|
81
|
+
return processed_data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def serialize_mixed_data(data: Any, bidi_component_proto: BidiComponentProto) -> None:
|
|
85
|
+
"""Serialize mixed data with automatic dataframe detection into a protobuf message.
|
|
86
|
+
|
|
87
|
+
This function detects dataframe-like objects in the first level of a dictionary,
|
|
88
|
+
extracts them into separate Arrow blobs, and populates a `MixedDataProto`
|
|
89
|
+
protobuf message for efficient serialization.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
data
|
|
94
|
+
The data structure to serialize. If it is a dictionary, its first
|
|
95
|
+
level will be scanned for dataframe-like objects.
|
|
96
|
+
bidi_component_proto
|
|
97
|
+
The protobuf message to populate with the serialized data.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
arrow_blobs: dict[str, bytes] = {}
|
|
101
|
+
|
|
102
|
+
# Only process dictionaries for automatic dataframe detection
|
|
103
|
+
if isinstance(data, dict):
|
|
104
|
+
processed_data = _extract_dataframes_from_dict(data, arrow_blobs)
|
|
105
|
+
else:
|
|
106
|
+
# For non-dict data (lists, tuples, etc.), pass through as-is
|
|
107
|
+
# We don't automatically detect dataframes in these structures
|
|
108
|
+
processed_data = data
|
|
109
|
+
|
|
110
|
+
if arrow_blobs:
|
|
111
|
+
# We have dataframes, use mixed data serialization
|
|
112
|
+
mixed_proto = MixedDataProto()
|
|
113
|
+
try:
|
|
114
|
+
mixed_proto.json = json.dumps(processed_data)
|
|
115
|
+
except TypeError:
|
|
116
|
+
# If JSON serialization fails (e.g., due to undetected dataframes),
|
|
117
|
+
# fall back to string representation
|
|
118
|
+
mixed_proto.json = json.dumps(str(processed_data))
|
|
119
|
+
|
|
120
|
+
# Add Arrow blobs to the protobuf
|
|
121
|
+
for ref_id, arrow_bytes in arrow_blobs.items():
|
|
122
|
+
mixed_proto.arrow_blobs[ref_id].data = arrow_bytes
|
|
123
|
+
|
|
124
|
+
bidi_component_proto.mixed.CopyFrom(mixed_proto)
|
|
125
|
+
else:
|
|
126
|
+
# No dataframes found, use regular JSON serialization
|
|
127
|
+
try:
|
|
128
|
+
bidi_component_proto.json = json.dumps(processed_data)
|
|
129
|
+
except TypeError:
|
|
130
|
+
# If JSON serialization fails (e.g., due to dataframes in lists/tuples),
|
|
131
|
+
# fall back to string representation
|
|
132
|
+
bidi_component_proto.json = json.dumps(str(processed_data))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def handle_deserialize(s: str | None) -> Any:
|
|
136
|
+
"""Deserialize a JSON string, returning the string itself if it's not valid JSON.
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
s
|
|
141
|
+
The string to deserialize.
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
Any
|
|
146
|
+
The deserialized JSON object, or the original string if parsing fails.
|
|
147
|
+
Returns `None` if the input is `None`.
|
|
148
|
+
|
|
149
|
+
"""
|
|
150
|
+
if s is None:
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
return json.loads(s)
|
|
154
|
+
except json.JSONDecodeError:
|
|
155
|
+
return s
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def deserialize_trigger_list(s: str | None) -> list[Any] | None:
|
|
159
|
+
"""Deserialize trigger aggregator payloads as a list.
|
|
160
|
+
|
|
161
|
+
For bidirectional components, the frontend always sends a JSON array of payload
|
|
162
|
+
objects. This deserializer normalizes older or singular payloads into a list
|
|
163
|
+
while preserving ``None`` for cleared values.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
s
|
|
168
|
+
The JSON string to deserialize, hopefully representing a list of payloads.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
list[Any] or None
|
|
173
|
+
A list of payloads, or `None` if the input was `None`.
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
value = handle_deserialize(s)
|
|
177
|
+
if value is None:
|
|
178
|
+
return None
|
|
179
|
+
if isinstance(value, list):
|
|
180
|
+
return value
|
|
181
|
+
return [value]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class BidiComponentSerde:
|
|
186
|
+
"""Serialization and deserialization logic for a bidirectional component.
|
|
187
|
+
|
|
188
|
+
This class handles the conversion of component state between the frontend
|
|
189
|
+
(JSON strings) and the backend (Python objects).
|
|
190
|
+
|
|
191
|
+
The canonical shape is a flat mapping of state keys to values.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
default
|
|
196
|
+
A dictionary of default values to be applied to the state when
|
|
197
|
+
deserializing, if the corresponding keys are not already present.
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
default: dict[str, Any] | None = None
|
|
202
|
+
|
|
203
|
+
def deserialize(self, ui_value: str | dict[str, Any] | None) -> BidiComponentState:
|
|
204
|
+
"""Deserialize the component's state from a frontend value.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
ui_value
|
|
209
|
+
The value received from the frontend, which can be a JSON string,
|
|
210
|
+
a dictionary, or `None`.
|
|
211
|
+
|
|
212
|
+
Returns
|
|
213
|
+
-------
|
|
214
|
+
BidiComponentState
|
|
215
|
+
The deserialized state as a flat mapping.
|
|
216
|
+
|
|
217
|
+
"""
|
|
218
|
+
# Normalize the incoming JSON payload into a dict. Any failure to decode
|
|
219
|
+
# (or an unexpected non-mapping structure) results in an empty mapping
|
|
220
|
+
# so that the returned type adheres to :class:`BidiComponentState`.
|
|
221
|
+
|
|
222
|
+
deserialized_value: dict[str, Any]
|
|
223
|
+
|
|
224
|
+
if isinstance(ui_value, dict):
|
|
225
|
+
deserialized_value = ui_value
|
|
226
|
+
elif isinstance(ui_value, str):
|
|
227
|
+
try:
|
|
228
|
+
parsed = json.loads(ui_value)
|
|
229
|
+
deserialized_value = parsed if isinstance(parsed, dict) else {}
|
|
230
|
+
except (json.JSONDecodeError, TypeError) as e:
|
|
231
|
+
_LOGGER.warning(
|
|
232
|
+
"Failed to deserialize component state from frontend: %s",
|
|
233
|
+
e,
|
|
234
|
+
exc_info=e,
|
|
235
|
+
)
|
|
236
|
+
deserialized_value = {}
|
|
237
|
+
else:
|
|
238
|
+
deserialized_value = {}
|
|
239
|
+
|
|
240
|
+
# Apply default values for keys that don't exist in the current state
|
|
241
|
+
if self.default is not None:
|
|
242
|
+
for default_key, default_value in self.default.items():
|
|
243
|
+
if default_key not in deserialized_value:
|
|
244
|
+
deserialized_value[default_key] = default_value
|
|
245
|
+
|
|
246
|
+
state: BidiComponentState = cast(
|
|
247
|
+
"BidiComponentState", AttributeDictionary(deserialized_value)
|
|
248
|
+
)
|
|
249
|
+
return state
|
|
250
|
+
|
|
251
|
+
def serialize(self, value: Any) -> str:
|
|
252
|
+
"""Serialize the component's state into a JSON string for the frontend.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
value
|
|
257
|
+
The component state to serialize.
|
|
258
|
+
|
|
259
|
+
Returns
|
|
260
|
+
-------
|
|
261
|
+
str
|
|
262
|
+
A JSON string representation of the value.
|
|
263
|
+
|
|
264
|
+
"""
|
|
265
|
+
return json.dumps(value)
|