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.
Files changed (259) hide show
  1. streamlit/__init__.py +4 -1
  2. streamlit/column_config.py +2 -0
  3. streamlit/commands/navigation.py +7 -7
  4. streamlit/commands/page_config.py +4 -6
  5. streamlit/components/v1/custom_component.py +17 -42
  6. streamlit/components/v2/__init__.py +458 -0
  7. streamlit/components/v2/bidi_component/__init__.py +20 -0
  8. streamlit/components/v2/bidi_component/constants.py +29 -0
  9. streamlit/components/v2/bidi_component/main.py +386 -0
  10. streamlit/components/v2/bidi_component/serialization.py +265 -0
  11. streamlit/components/v2/bidi_component/state.py +92 -0
  12. streamlit/components/v2/component_definition_resolver.py +143 -0
  13. streamlit/components/v2/component_file_watcher.py +403 -0
  14. streamlit/components/v2/component_manager.py +431 -0
  15. streamlit/components/v2/component_manifest_handler.py +122 -0
  16. streamlit/components/v2/component_path_utils.py +245 -0
  17. streamlit/components/v2/component_registry.py +409 -0
  18. streamlit/components/v2/get_bidi_component_manager.py +51 -0
  19. streamlit/components/v2/manifest_scanner.py +615 -0
  20. streamlit/components/v2/presentation.py +198 -0
  21. streamlit/components/v2/types.py +324 -0
  22. streamlit/config.py +741 -32
  23. streamlit/config_option.py +4 -1
  24. streamlit/config_util.py +650 -1
  25. streamlit/connections/base_connection.py +4 -2
  26. streamlit/dataframe_util.py +18 -10
  27. streamlit/delta_generator.py +8 -7
  28. streamlit/delta_generator_singletons.py +3 -1
  29. streamlit/deprecation_util.py +17 -6
  30. streamlit/elements/arrow.py +90 -42
  31. streamlit/elements/deck_gl_json_chart.py +98 -39
  32. streamlit/elements/dialog_decorator.py +2 -1
  33. streamlit/elements/exception.py +3 -1
  34. streamlit/elements/form.py +6 -6
  35. streamlit/elements/graphviz_chart.py +24 -9
  36. streamlit/elements/heading.py +3 -5
  37. streamlit/elements/iframe.py +0 -2
  38. streamlit/elements/image.py +12 -13
  39. streamlit/elements/layouts.py +89 -22
  40. streamlit/elements/lib/built_in_chart_utils.py +95 -31
  41. streamlit/elements/lib/color_util.py +8 -18
  42. streamlit/elements/lib/column_config_utils.py +9 -8
  43. streamlit/elements/lib/column_types.py +595 -148
  44. streamlit/elements/lib/dialog.py +3 -2
  45. streamlit/elements/lib/image_utils.py +3 -5
  46. streamlit/elements/lib/layout_utils.py +50 -13
  47. streamlit/elements/lib/mutable_status_container.py +2 -2
  48. streamlit/elements/lib/options_selector_utils.py +2 -2
  49. streamlit/elements/lib/pandas_styler_utils.py +30 -14
  50. streamlit/elements/lib/utils.py +21 -9
  51. streamlit/elements/map.py +81 -40
  52. streamlit/elements/media.py +7 -7
  53. streamlit/elements/metric.py +11 -35
  54. streamlit/elements/pdf.py +2 -4
  55. streamlit/elements/plotly_chart.py +142 -26
  56. streamlit/elements/progress.py +2 -4
  57. streamlit/elements/pyplot.py +6 -6
  58. streamlit/elements/space.py +113 -0
  59. streamlit/elements/vega_charts.py +400 -143
  60. streamlit/elements/widgets/audio_input.py +52 -4
  61. streamlit/elements/widgets/button.py +29 -29
  62. streamlit/elements/widgets/button_group.py +33 -6
  63. streamlit/elements/widgets/camera_input.py +3 -4
  64. streamlit/elements/widgets/chat.py +7 -0
  65. streamlit/elements/widgets/checkbox.py +1 -0
  66. streamlit/elements/widgets/color_picker.py +1 -0
  67. streamlit/elements/widgets/data_editor.py +34 -29
  68. streamlit/elements/widgets/file_uploader.py +6 -10
  69. streamlit/elements/widgets/multiselect.py +14 -3
  70. streamlit/elements/widgets/number_input.py +5 -4
  71. streamlit/elements/widgets/radio.py +10 -2
  72. streamlit/elements/widgets/select_slider.py +8 -4
  73. streamlit/elements/widgets/selectbox.py +9 -2
  74. streamlit/elements/widgets/slider.py +38 -41
  75. streamlit/elements/widgets/text_widgets.py +6 -0
  76. streamlit/elements/widgets/time_widgets.py +15 -12
  77. streamlit/elements/write.py +28 -23
  78. streamlit/emojis.py +1 -1
  79. streamlit/errors.py +115 -0
  80. streamlit/git_util.py +65 -43
  81. streamlit/hello/hello.py +8 -0
  82. streamlit/hello/utils.py +2 -1
  83. streamlit/material_icon_names.py +1 -1
  84. streamlit/navigation/page.py +4 -1
  85. streamlit/proto/ArrowData_pb2.py +27 -0
  86. streamlit/proto/ArrowData_pb2.pyi +46 -0
  87. streamlit/proto/Arrow_pb2.py +10 -8
  88. streamlit/proto/Arrow_pb2.pyi +31 -2
  89. streamlit/proto/AudioInput_pb2.py +2 -2
  90. streamlit/proto/AudioInput_pb2.pyi +6 -2
  91. streamlit/proto/BidiComponent_pb2.py +34 -0
  92. streamlit/proto/BidiComponent_pb2.pyi +153 -0
  93. streamlit/proto/Block_pb2.py +11 -11
  94. streamlit/proto/Block_pb2.pyi +9 -1
  95. streamlit/proto/DeckGlJsonChart_pb2.py +10 -4
  96. streamlit/proto/DeckGlJsonChart_pb2.pyi +9 -3
  97. streamlit/proto/Element_pb2.py +5 -3
  98. streamlit/proto/Element_pb2.pyi +14 -4
  99. streamlit/proto/HeightConfig_pb2.py +2 -2
  100. streamlit/proto/HeightConfig_pb2.pyi +6 -3
  101. streamlit/proto/NewSession_pb2.py +18 -16
  102. streamlit/proto/NewSession_pb2.pyi +158 -6
  103. streamlit/proto/PlotlyChart_pb2.py +8 -6
  104. streamlit/proto/PlotlyChart_pb2.pyi +3 -1
  105. streamlit/proto/Space_pb2.py +27 -0
  106. streamlit/proto/Space_pb2.pyi +42 -0
  107. streamlit/proto/WidgetStates_pb2.py +2 -2
  108. streamlit/proto/WidgetStates_pb2.pyi +13 -3
  109. streamlit/proto/WidthConfig_pb2.py +2 -2
  110. streamlit/proto/WidthConfig_pb2.pyi +6 -3
  111. streamlit/runtime/app_session.py +45 -6
  112. streamlit/runtime/caching/cache_data_api.py +4 -4
  113. streamlit/runtime/caching/cache_errors.py +4 -1
  114. streamlit/runtime/caching/cache_resource_api.py +3 -2
  115. streamlit/runtime/caching/cache_utils.py +2 -1
  116. streamlit/runtime/caching/cached_message_replay.py +3 -3
  117. streamlit/runtime/caching/hashing.py +3 -4
  118. streamlit/runtime/caching/legacy_cache_api.py +2 -1
  119. streamlit/runtime/connection_factory.py +1 -3
  120. streamlit/runtime/forward_msg_queue.py +4 -1
  121. streamlit/runtime/fragment.py +2 -1
  122. streamlit/runtime/memory_media_file_storage.py +1 -1
  123. streamlit/runtime/metrics_util.py +6 -2
  124. streamlit/runtime/runtime.py +14 -0
  125. streamlit/runtime/scriptrunner/exec_code.py +2 -1
  126. streamlit/runtime/scriptrunner/script_runner.py +2 -2
  127. streamlit/runtime/scriptrunner_utils/script_run_context.py +3 -6
  128. streamlit/runtime/secrets.py +2 -4
  129. streamlit/runtime/session_manager.py +3 -1
  130. streamlit/runtime/state/common.py +30 -5
  131. streamlit/runtime/state/presentation.py +85 -0
  132. streamlit/runtime/state/safe_session_state.py +2 -2
  133. streamlit/runtime/state/session_state.py +220 -16
  134. streamlit/runtime/state/widgets.py +19 -3
  135. streamlit/runtime/theme_util.py +148 -0
  136. streamlit/runtime/websocket_session_manager.py +3 -1
  137. streamlit/source_util.py +2 -2
  138. streamlit/static/index.html +2 -2
  139. streamlit/static/manifest.json +244 -227
  140. streamlit/static/static/css/{index.C8X8rNzw.css → index.BpABIXK9.css} +1 -1
  141. streamlit/static/static/css/index.DgR7E2CV.css +1 -0
  142. streamlit/static/static/js/{ErrorOutline.esm.DcGrhbBP.js → ErrorOutline.esm.YoJdlW1p.js} +1 -1
  143. streamlit/static/static/js/{FileDownload.esm.DgBvV6Pq.js → FileDownload.esm.Ddx8VEYy.js} +1 -1
  144. streamlit/static/static/js/{FileHelper.M6AAaeuA.js → FileHelper.90EtOmj9.js} +1 -1
  145. streamlit/static/static/js/{FormClearHelper.DHh1GFzm.js → FormClearHelper.BB1Km6eP.js} +1 -1
  146. streamlit/static/static/js/InputInstructions.jhH15PqV.js +1 -0
  147. streamlit/static/static/js/{Particles.DDVT-6Qc.js → Particles.DUsputn1.js} +1 -1
  148. streamlit/static/static/js/{ProgressBar.BEY0cXXV.js → ProgressBar.DLY8H6nE.js} +2 -2
  149. streamlit/static/static/js/Toolbar.D8nHCkuz.js +1 -0
  150. streamlit/static/static/js/{base-input.CK3UVGp1.js → base-input.CJGiNqed.js} +3 -3
  151. streamlit/static/static/js/{checkbox.D8W881TL.js → checkbox.Cpdd482O.js} +1 -1
  152. streamlit/static/static/js/{createSuper.B6W-Dh9S.js → createSuper.CuQIogbW.js} +1 -1
  153. streamlit/static/static/js/data-grid-overlay-editor.2Ufgxc6y.js +1 -0
  154. streamlit/static/static/js/{downloader.DiKpuU_S.js → downloader.CN0K7xlu.js} +1 -1
  155. streamlit/static/static/js/{es6.B8zRNPZ-.js → es6.BJcsVXQ0.js} +2 -2
  156. streamlit/static/static/js/{iframeResizer.contentWindow.DIewJmmh.js → iframeResizer.contentWindow.XzUvQqcZ.js} +1 -1
  157. streamlit/static/static/js/index.B1ZQh4P1.js +1 -0
  158. streamlit/static/static/js/index.BKstZk0M.js +27 -0
  159. streamlit/static/static/js/{index.Bte_9Lyq.js → index.BMcFsUee.js} +1 -1
  160. streamlit/static/static/js/{index.qhs54UAB.js → index.BR-IdcTb.js} +1 -1
  161. streamlit/static/static/js/{index.CejBxbg1.js → index.B_dWA3vd.js} +1 -1
  162. streamlit/static/static/js/{index.D5naqx-J.js → index.BgnZEMVh.js} +1 -1
  163. streamlit/static/static/js/{index.C7fRKRs4.js → index.BohqXifI.js} +1 -1
  164. streamlit/static/static/js/{index.cnnXF7xQ.js → index.Br5nxKNj.js} +1 -1
  165. streamlit/static/static/js/index.BrIKVbNc.js +3 -0
  166. streamlit/static/static/js/index.BtWUPzle.js +1 -0
  167. streamlit/static/static/js/index.C0RLraek.js +1 -0
  168. streamlit/static/static/js/{index.CP5TD2z1.js → index.CAIjskgG.js} +1 -1
  169. streamlit/static/static/js/{index.CD8HuT3N.js → index.CAj-7vWz.js} +135 -162
  170. streamlit/static/static/js/{index.DtYN2x4k.js → index.CMtEit2O.js} +1 -1
  171. streamlit/static/static/js/index.CkRlykEE.js +12 -0
  172. streamlit/static/static/js/{index.Ts_0SdB9.js → index.CmN3FXfI.js} +2 -2
  173. streamlit/static/static/js/{index.BnEpvLEz.js → index.CwbFI1_-.js} +1 -1
  174. streamlit/static/static/js/{index.CcJf6BCU.js → index.CxIUUfab.js} +27 -27
  175. streamlit/static/static/js/index.D2KPNy7e.js +1 -0
  176. streamlit/static/static/js/{index.Ch7MBCx0.js → index.D3GPA5k4.js} +47 -47
  177. streamlit/static/static/js/{index.ho6NIXGl.js → index.DGAh7DMq.js} +1 -1
  178. streamlit/static/static/js/index.DKb_NvmG.js +197 -0
  179. streamlit/static/static/js/{index.CvYYtxD_.js → index.DMqgUYKq.js} +1 -1
  180. streamlit/static/static/js/{index.zecpGxtj.js → index.DOFlg3dS.js} +1 -1
  181. streamlit/static/static/js/{index.B9mjBcgE.js → index.DPUXkcQL.js} +1 -1
  182. streamlit/static/static/js/index.DX1xY89g.js +1 -0
  183. streamlit/static/static/js/index.DYATBCsq.js +2 -0
  184. streamlit/static/static/js/{index.D2-atlaQ.js → index.DaSmGJ76.js} +3 -3
  185. streamlit/static/static/js/index.Dd7bMeLP.js +1 -0
  186. streamlit/static/static/js/{index.4eF4NxG2.js → index.DjmmgI5U.js} +1 -1
  187. streamlit/static/static/js/index.Dq56CyM2.js +1 -0
  188. streamlit/static/static/js/index.DuiXaS5_.js +7 -0
  189. streamlit/static/static/js/index.DvFidMLe.js +2 -0
  190. streamlit/static/static/js/{index.452cqrrL.js → index.DwkhC5Pc.js} +1 -1
  191. streamlit/static/static/js/{index.Dk4C7X3i.js → index.Q-3sFn1v.js} +1 -1
  192. streamlit/static/static/js/{index.CjXWwH-y.js → index.QJ5QO9sJ.js} +1 -1
  193. streamlit/static/static/js/{index.B6U8LQo3.js → index.VwTaeety.js} +1 -1
  194. streamlit/static/static/js/index.YOqQbeX8.js +1 -0
  195. streamlit/static/static/js/{input.nzVJphXi.js → input.D4MN_FzN.js} +1 -1
  196. streamlit/static/static/js/{memory.CjCgTQz3.js → memory.DrZjtdGT.js} +1 -1
  197. streamlit/static/static/js/{number-overlay-editor.DaRFzZEO.js → number-overlay-editor.DRwAw1In.js} +1 -1
  198. streamlit/static/static/js/{possibleConstructorReturn.DgiPnZ9N.js → possibleConstructorReturn.exeeJQEP.js} +1 -1
  199. streamlit/static/static/js/record.B-tDciZb.js +1 -0
  200. streamlit/static/static/js/{sandbox.mithfq7Z.js → sandbox.ClO3IuUr.js} +1 -1
  201. streamlit/static/static/js/{timepicker.Dbl5KFh6.js → timepicker.DAhu-vcF.js} +4 -4
  202. streamlit/static/static/js/{toConsumableArray.D-Dx88BQ.js → toConsumableArray.DNbljYEC.js} +1 -1
  203. streamlit/static/static/js/{uniqueId.Bh26R_3S.js → uniqueId.oG4Gvj1v.js} +1 -1
  204. streamlit/static/static/js/{useBasicWidgetState.DeK-QJpD.js → useBasicWidgetState.D6sOH6oI.js} +1 -1
  205. streamlit/static/static/js/{useTextInputAutoExpand.4iAdLWD-.js → useTextInputAutoExpand.4u3_GcuN.js} +2 -2
  206. streamlit/static/static/js/{useUpdateUiValue.CmT7_nJN.js → useUpdateUiValue.F2R3eTeR.js} +1 -1
  207. streamlit/static/static/js/wavesurfer.esm.vI8Eid4k.js +73 -0
  208. streamlit/static/static/js/withFullScreenWrapper.zothJIsI.js +1 -0
  209. streamlit/static/static/media/MaterialSymbols-Rounded.C7IFxh57.woff2 +0 -0
  210. streamlit/string_util.py +56 -1
  211. streamlit/testing/v1/app_test.py +2 -2
  212. streamlit/testing/v1/element_tree.py +23 -9
  213. streamlit/testing/v1/util.py +2 -2
  214. streamlit/type_util.py +3 -4
  215. streamlit/url_util.py +1 -3
  216. streamlit/user_info.py +1 -2
  217. streamlit/util.py +3 -1
  218. streamlit/watcher/event_based_path_watcher.py +23 -12
  219. streamlit/watcher/local_sources_watcher.py +11 -1
  220. streamlit/watcher/path_watcher.py +9 -6
  221. streamlit/watcher/polling_path_watcher.py +4 -1
  222. streamlit/watcher/util.py +2 -2
  223. streamlit/web/bootstrap.py +0 -31
  224. streamlit/web/cli.py +51 -22
  225. streamlit/web/server/bidi_component_request_handler.py +193 -0
  226. streamlit/web/server/component_file_utils.py +97 -0
  227. streamlit/web/server/component_request_handler.py +8 -21
  228. streamlit/web/server/oidc_mixin.py +3 -1
  229. streamlit/web/server/routes.py +18 -5
  230. streamlit/web/server/server.py +10 -0
  231. streamlit/web/server/server_util.py +3 -1
  232. streamlit/web/server/upload_file_request_handler.py +3 -1
  233. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/METADATA +4 -5
  234. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/RECORD +238 -209
  235. streamlit/static/static/css/index.COe1010n.css +0 -1
  236. streamlit/static/static/js/Hooks.DGu1od_L.js +0 -1
  237. streamlit/static/static/js/InputInstructions.z6sVgyYt.js +0 -1
  238. streamlit/static/static/js/Toolbar.DSnK1fUh.js +0 -1
  239. streamlit/static/static/js/data-grid-overlay-editor.DRTHOydk.js +0 -1
  240. streamlit/static/static/js/index.BXYmrqnf.js +0 -1
  241. streamlit/static/static/js/index.B_8AnktO.js +0 -1
  242. streamlit/static/static/js/index.Bl7zGQSh.js +0 -7
  243. streamlit/static/static/js/index.BnJIOYn9.js +0 -73
  244. streamlit/static/static/js/index.C1HcTl5K.js +0 -1
  245. streamlit/static/static/js/index.C7lSmSOP.js +0 -1
  246. streamlit/static/static/js/index.C_tmcx4B.js +0 -1
  247. streamlit/static/static/js/index.D3K5nOu9.js +0 -197
  248. streamlit/static/static/js/index.DkKT3LUI.js +0 -1
  249. streamlit/static/static/js/index.MTPPBDHk.js +0 -2
  250. streamlit/static/static/js/index.pqW9AMJD.js +0 -3
  251. streamlit/static/static/js/index.urHgTgMQ.js +0 -12
  252. streamlit/static/static/js/index.wzkv_11M.js +0 -1
  253. streamlit/static/static/js/index.yF5AncHY.js +0 -1
  254. streamlit/static/static/js/withFullScreenWrapper.DLp1ENGm.js +0 -1
  255. streamlit/static/static/media/MaterialSymbols-Rounded.CBxVaFdk.woff2 +0 -0
  256. {streamlit-1.49.1.data → streamlit-1.51.0.data}/scripts/streamlit.cmd +0 -0
  257. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/WHEEL +0 -0
  258. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/entry_points.txt +0 -0
  259. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,245 @@
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
+ """Security-hardened path utilities for Component v2.
16
+
17
+ This module centralizes path validation and resolution logic used by the
18
+ Component v2 registration and file access code paths. All helpers here are
19
+ designed to prevent path traversal, insecure prefix checks, and symlink escapes
20
+ outside of a declared package root.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ from pathlib import Path
27
+ from typing import Final
28
+
29
+ from streamlit.errors import StreamlitComponentRegistryError
30
+ from streamlit.logger import get_logger
31
+
32
+ _LOGGER: Final = get_logger(__name__)
33
+
34
+
35
+ class ComponentPathUtils:
36
+ """Utility class for component path operations and security validation."""
37
+
38
+ @staticmethod
39
+ def has_glob_characters(path: str) -> bool:
40
+ """Check if a path contains glob pattern characters.
41
+
42
+ Parameters
43
+ ----------
44
+ path : str
45
+ The path to check
46
+
47
+ Returns
48
+ -------
49
+ bool
50
+ True if the path contains glob characters
51
+ """
52
+ return any(char in path for char in ["*", "?", "[", "]"])
53
+
54
+ @staticmethod
55
+ def validate_path_security(path: str) -> None:
56
+ """Validate that a path doesn't contain security vulnerabilities.
57
+
58
+ Parameters
59
+ ----------
60
+ path : str
61
+ The path to validate
62
+
63
+ Raises
64
+ ------
65
+ StreamlitComponentRegistryError
66
+ If the path contains security vulnerabilities like path traversal attempts
67
+ """
68
+ ComponentPathUtils._assert_relative_no_traversal(path, label="component paths")
69
+
70
+ @staticmethod
71
+ def resolve_glob_pattern(pattern: str, package_root: Path) -> Path:
72
+ """Resolve a glob pattern to a single file path with security checks.
73
+
74
+ Parameters
75
+ ----------
76
+ pattern : str
77
+ The glob pattern to resolve
78
+ package_root : Path
79
+ The package root directory for security validation
80
+
81
+ Returns
82
+ -------
83
+ Path
84
+ The resolved file path
85
+
86
+ Raises
87
+ ------
88
+ StreamlitComponentRegistryError
89
+ If zero or more than one file matches the pattern, or if security
90
+ checks fail (path traversal attempts)
91
+ """
92
+ # Ensure pattern is relative and doesn't contain path traversal attempts
93
+ ComponentPathUtils._assert_relative_no_traversal(pattern, label="glob patterns")
94
+
95
+ # Use glob from the package root so subdirectory patterns are handled correctly
96
+ matching_files = list(package_root.glob(pattern))
97
+
98
+ # Ensure all matched files are within package_root (security check)
99
+ validated_files = []
100
+ for file_path in matching_files:
101
+ try:
102
+ # Resolve to absolute path and check if it's within package_root
103
+ resolved_path = file_path.resolve()
104
+ package_root_resolved = package_root.resolve()
105
+
106
+ # Check if the resolved path is within the package root using
107
+ # pathlib's relative path check to avoid prefix-matching issues
108
+ if not resolved_path.is_relative_to(package_root_resolved):
109
+ _LOGGER.warning(
110
+ "Skipping file outside package root: %s", resolved_path
111
+ )
112
+ continue
113
+
114
+ validated_files.append(resolved_path)
115
+ except (OSError, ValueError) as e:
116
+ _LOGGER.warning("Failed to resolve path %s: %s", file_path, e)
117
+ continue
118
+
119
+ # Ensure exactly one file matches
120
+ if len(validated_files) == 0:
121
+ raise StreamlitComponentRegistryError(
122
+ f"No files found matching pattern '{pattern}' in package root {package_root}"
123
+ )
124
+ if len(validated_files) > 1:
125
+ file_list = ", ".join(str(f) for f in validated_files)
126
+ raise StreamlitComponentRegistryError(
127
+ f"Multiple files found matching pattern '{pattern}': {file_list}. "
128
+ "Exactly one file must match the pattern."
129
+ )
130
+
131
+ return Path(validated_files[0])
132
+
133
+ @staticmethod
134
+ def _assert_relative_no_traversal(path: str, *, label: str) -> None:
135
+ """Raise if ``path`` is absolute or contains ``..`` segments.
136
+
137
+ Parameters
138
+ ----------
139
+ path : str
140
+ Path string to validate.
141
+ label : str
142
+ Human-readable label used in error messages (e.g., "component paths").
143
+ """
144
+ # Absolute path checks (POSIX, Windows drive-letter, UNC)
145
+ is_windows_drive_abs = (
146
+ len(path) >= 3
147
+ and path[0].isalpha()
148
+ and path[1] == ":"
149
+ and path[2] in ("/", "\\")
150
+ )
151
+ is_unc_abs = path.startswith("\\\\")
152
+
153
+ # Consider rooted backslash paths "\\dir" as absolute on Windows-like inputs
154
+ is_rooted_backslash = path.startswith("\\") and not is_unc_abs
155
+
156
+ if (
157
+ os.path.isabs(path)
158
+ or is_windows_drive_abs
159
+ or is_unc_abs
160
+ or is_rooted_backslash
161
+ ):
162
+ raise StreamlitComponentRegistryError(
163
+ f"Absolute paths are not allowed in {label}: {path}"
164
+ )
165
+
166
+ # Segment-based traversal detection to avoid false positives (e.g. "file..js")
167
+ normalized = path.replace("\\", "/")
168
+ segments = [seg for seg in normalized.split("/") if seg != ""]
169
+ if any(seg == ".." for seg in segments):
170
+ raise StreamlitComponentRegistryError(
171
+ f"Path traversal attempts are not allowed in {label}: {path}"
172
+ )
173
+
174
+ @staticmethod
175
+ def ensure_within_root(abs_path: Path, root: Path, *, kind: str) -> None:
176
+ """Ensure that abs_path is within root; raise if not.
177
+
178
+ Parameters
179
+ ----------
180
+ abs_path : Path
181
+ Absolute file path
182
+ root : Path
183
+ Root directory path
184
+ kind : str
185
+ Human-readable descriptor for error messages (e.g., "js" or "css")
186
+
187
+ Raises
188
+ ------
189
+ StreamlitComponentRegistryError
190
+ If the path cannot be resolved or if the resolved path does not
191
+ reside within ``root`` after following symlinks.
192
+ """
193
+ try:
194
+ resolved = abs_path.resolve()
195
+ root_resolved = root.resolve()
196
+ except Exception as e:
197
+ raise StreamlitComponentRegistryError(
198
+ f"Failed to resolve {kind} path '{abs_path}': {e}"
199
+ ) from e
200
+
201
+ # Use Path.is_relative_to to avoid insecure prefix-based checks
202
+ if not resolved.is_relative_to(root_resolved):
203
+ raise StreamlitComponentRegistryError(
204
+ f"{kind} path '{abs_path}' is outside the declared asset_dir '{root}'."
205
+ )
206
+
207
+ @staticmethod
208
+ def looks_like_inline_content(value: str) -> bool:
209
+ r"""Heuristic to detect inline JS/CSS content strings.
210
+
211
+ Treat a string as a file path ONLY if it looks path-like:
212
+ - Does not contain newlines
213
+ - Contains glob characters (*, ?, [, ])
214
+ - Starts with ./, /, or \
215
+ - Contains a path separator ("/" or "\\")
216
+ - Or ends with a common asset extension like .js, .mjs, .cjs, or .css
217
+
218
+ Otherwise, treat it as inline content.
219
+
220
+ Parameters
221
+ ----------
222
+ value : str
223
+ The string to classify as inline content or a file path.
224
+
225
+ Returns
226
+ -------
227
+ bool
228
+ True if ``value`` looks like inline content; False if it looks like a
229
+ file path.
230
+ """
231
+ s = value.strip()
232
+ # If the value contains newlines, it's definitely inline content
233
+ if "\n" in s or "\r" in s:
234
+ return True
235
+ # Glob patterns indicate path-like
236
+ if ComponentPathUtils.has_glob_characters(s):
237
+ return False
238
+ # Obvious path prefixes
239
+ if s.startswith(("./", "/", "\\")):
240
+ return False
241
+ # Any path separator
242
+ if "/" in s or "\\" in s:
243
+ return False
244
+
245
+ return not (s.lower().endswith((".js", ".css", ".mjs", ".cjs")))
@@ -0,0 +1,409 @@
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
+
16
+ """Component registry for Custom Components v2.
17
+
18
+ This module defines the data model and in-memory registry for Custom Components
19
+ v2. During development, component assets (JS/CSS/HTML) may change on disk as
20
+ build tools produce new outputs.
21
+
22
+ See Also
23
+ --------
24
+ - :class:`streamlit.components.v2.component_file_watcher.ComponentFileWatcher`
25
+ for directory watching and change notifications.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import os
31
+ import threading
32
+ from dataclasses import dataclass, field
33
+ from typing import TYPE_CHECKING, Any, Final
34
+
35
+ from streamlit.components.v2.component_path_utils import ComponentPathUtils
36
+ from streamlit.errors import StreamlitComponentRegistryError
37
+ from streamlit.logger import get_logger
38
+
39
+ if TYPE_CHECKING:
40
+ from collections.abc import MutableMapping
41
+
42
+
43
+ _LOGGER: Final = get_logger(__name__)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class BidiComponentDefinition:
48
+ """Definition of a bidirectional component V2.
49
+
50
+ The definition holds inline content or file references for HTML, CSS, and
51
+ JavaScript, plus metadata used by the runtime to serve assets. When CSS/JS
52
+ are provided as file paths, their asset-dir-relative URLs are exposed via
53
+ ``css_url`` and ``js_url`` (or can be overridden with
54
+ ``css_asset_relative_path``/``js_asset_relative_path``).
55
+
56
+ Parameters
57
+ ----------
58
+ name : str
59
+ A short, descriptive name for the component.
60
+ html : str or None, optional
61
+ HTML content as a string.
62
+ css : str or None, optional
63
+ Inline CSS content or an absolute/relative path to a ``.css`` file.
64
+ Relative paths are interpreted as asset-dir-relative and validated to
65
+ reside within the component's ``asset_dir``. Absolute paths are rejected
66
+ by the API.
67
+ js : str or None, optional
68
+ Inline JavaScript content or an absolute/relative path to a ``.js``
69
+ file. Relative paths are interpreted as asset-dir-relative and validated
70
+ to reside within the component's ``asset_dir``. Absolute paths are
71
+ rejected by the API.
72
+ css_asset_relative_path : str or None, optional
73
+ Asset-dir-relative URL path to use when serving the CSS file. If not
74
+ provided, the filename from ``css`` is used when ``css`` is file-backed.
75
+ js_asset_relative_path : str or None, optional
76
+ Asset-dir-relative URL path to use when serving the JS file. If not
77
+ provided, the filename from ``js`` is used when ``js`` is file-backed.
78
+ """
79
+
80
+ name: str
81
+ html: str | None = None
82
+ css: str | None = None
83
+ js: str | None = None
84
+ # Store processed content and metadata
85
+ _has_css_path: bool = field(default=False, init=False, repr=False)
86
+ _has_js_path: bool = field(default=False, init=False, repr=False)
87
+ _source_paths: dict[str, str] = field(default_factory=dict, init=False, repr=False)
88
+ # Asset-dir-relative paths used for frontend loading. These represent the
89
+ # URL path segment under the component's declared asset_dir (e.g. "build/index.js")
90
+ # and are independent of the on-disk absolute file path stored in css/js.
91
+ css_asset_relative_path: str | None = None
92
+ js_asset_relative_path: str | None = None
93
+
94
+ def __post_init__(self) -> None:
95
+ # Keep track of source paths for content loaded from files
96
+ source_paths = {}
97
+
98
+ # Store CSS and JS paths if provided
99
+ is_css_path, css_path = self._is_file_path(self.css)
100
+ is_js_path, js_path = self._is_file_path(self.js)
101
+
102
+ if css_path:
103
+ source_paths["css"] = os.path.dirname(css_path)
104
+ if js_path:
105
+ source_paths["js"] = os.path.dirname(js_path)
106
+
107
+ object.__setattr__(self, "_has_css_path", is_css_path)
108
+ object.__setattr__(self, "_has_js_path", is_js_path)
109
+ object.__setattr__(self, "_source_paths", source_paths)
110
+
111
+ # Allow empty definitions to support manifest-registered components that
112
+ # declare only an asset sandbox (asset_dir) without inline or file-backed
113
+ # entry content. Runtime API calls can later provide js/css/html.
114
+
115
+ def _is_file_path(self, content: str | None) -> tuple[bool, str | None]:
116
+ """Determine whether ``content`` is a filesystem path and resolve it.
117
+
118
+ For string inputs that look like paths (contain separators, prefixes, or
119
+ have common asset extensions), values are normally provided by the v2
120
+ public API, which resolves and validates asset-dir-relative inputs and
121
+ passes absolute paths here. When this dataclass is constructed
122
+ internally, callers must supply already-resolved absolute paths that
123
+ have passed the same validation rules upstream. Relative paths are not
124
+ accepted here.
125
+
126
+ Parameters
127
+ ----------
128
+ content : str or None
129
+ The potential inline content or path.
130
+
131
+ Returns
132
+ -------
133
+ tuple[bool, str | None]
134
+ ``(is_path, abs_path)`` where ``is_path`` indicates whether the
135
+ input was treated as a path and ``abs_path`` is the resolved
136
+ absolute path if a path, otherwise ``None``.
137
+
138
+ Raises
139
+ ------
140
+ ValueError
141
+ If ``content`` is treated as a path but the file does not exist, or
142
+ if a non-absolute, path-like string is provided.
143
+ """
144
+ if content is None:
145
+ return False, None
146
+
147
+ # Determine if it's a file path or inline content for strings
148
+ if isinstance(content, str):
149
+ stripped = content.strip()
150
+ is_likely_path = not ComponentPathUtils.looks_like_inline_content(stripped)
151
+
152
+ if is_likely_path:
153
+ if os.path.isabs(content):
154
+ abs_path = content
155
+ if not os.path.exists(abs_path):
156
+ raise ValueError(f"File does not exist: {abs_path}")
157
+ return True, abs_path
158
+ # Relative, path-like strings are not accepted at this layer.
159
+ raise ValueError(
160
+ "Relative file paths are not accepted in BidiComponentDefinition; "
161
+ "pass absolute, pre-validated paths from the v2 API."
162
+ )
163
+
164
+ # If we get here, it's content, not a path
165
+ return False, None
166
+
167
+ @property
168
+ def css_url(self) -> str | None:
169
+ """Return the asset-dir-relative URL path for CSS when file-backed.
170
+
171
+ When present, servers construct
172
+ ``/_stcore/bidi-components/<component>/<css_url>`` using this value. If
173
+ ``css_asset_relative_path`` is specified, it takes precedence over the
174
+ filename derived from ``css``.
175
+ """
176
+ return self._derive_asset_url(
177
+ has_path=self._has_css_path,
178
+ value=self.css,
179
+ override=self.css_asset_relative_path,
180
+ )
181
+
182
+ @property
183
+ def js_url(self) -> str | None:
184
+ """Return the asset-dir-relative URL path for JS when file-backed.
185
+
186
+ When present, servers construct
187
+ ``/_stcore/bidi-components/<component>/<js_url>`` using this value. If
188
+ ``js_asset_relative_path`` is specified, it takes precedence over the
189
+ filename derived from ``js``.
190
+ """
191
+ return self._derive_asset_url(
192
+ has_path=self._has_js_path,
193
+ value=self.js,
194
+ override=self.js_asset_relative_path,
195
+ )
196
+
197
+ def _derive_asset_url(
198
+ self, *, has_path: bool, value: str | None, override: str | None
199
+ ) -> str | None:
200
+ """Compute asset-dir-relative URL for a file-backed asset.
201
+
202
+ Parameters
203
+ ----------
204
+ has_path
205
+ Whether the value refers to a file path.
206
+ value
207
+ The css/js field value (inline string or path).
208
+ override
209
+ Optional explicit asset-dir-relative override.
210
+
211
+ Returns
212
+ -------
213
+ str or None
214
+ The derived URL path or ``None`` if not file-backed.
215
+ """
216
+ if not has_path:
217
+ return None
218
+ # Prefer explicit URL override if provided (relative to asset_dir)
219
+ if override:
220
+ return override
221
+ # Fallback: preserve relative subpath if the provided path is relative;
222
+ # otherwise default to the basename for absolute paths. Normalize
223
+ # leading "./" to avoid awkward prefixes in URLs.
224
+ path_str = str(value)
225
+ if os.path.isabs(path_str):
226
+ return os.path.basename(path_str)
227
+ norm = path_str.replace("\\", "/").removeprefix("./")
228
+ # If there's a subpath remaining, preserve it; otherwise use basename
229
+ return norm if "/" in norm else os.path.basename(norm)
230
+
231
+ @property
232
+ def css_content(self) -> str | None:
233
+ """Return inline CSS content or ``None`` if file-backed or missing."""
234
+ if self._has_css_path or self.css is None:
235
+ return None
236
+ # Return as string if it's not a path
237
+ return str(self.css)
238
+
239
+ @property
240
+ def js_content(self) -> str | None:
241
+ """Return inline JavaScript content or ``None`` if file-backed or missing."""
242
+ if self._has_js_path or self.js is None:
243
+ return None
244
+ # Return as string if it's not a path
245
+ return str(self.js)
246
+
247
+ @property
248
+ def html_content(self) -> str | None:
249
+ """Return inline HTML content or ``None`` if not provided."""
250
+ return self.html
251
+
252
+ @property
253
+ def source_paths(self) -> dict[str, str]:
254
+ """Return source directories for file-backed CSS/JS content.
255
+
256
+ The returned mapping contains keys like ``"js"`` and ``"css"`` with the
257
+ directory path from which each was loaded.
258
+ """
259
+ return self._source_paths
260
+
261
+
262
+ class BidiComponentRegistry:
263
+ """Registry for bidirectional components V2.
264
+
265
+ The registry stores and updates :class:`BidiComponentDefinition` instances in
266
+ a thread-safe mapping guarded by a lock.
267
+ """
268
+
269
+ def __init__(self) -> None:
270
+ """Initialize the component registry with an empty, thread-safe store."""
271
+ self._components: MutableMapping[str, BidiComponentDefinition] = {}
272
+ self._lock = threading.Lock()
273
+
274
+ def register_components_from_definitions(
275
+ self, component_definitions: dict[str, dict[str, Any]]
276
+ ) -> None:
277
+ """Register components from processed definition data.
278
+
279
+ Parameters
280
+ ----------
281
+ component_definitions : dict[str, dict[str, Any]]
282
+ Mapping from component identifier to definition data.
283
+ """
284
+ with self._lock:
285
+ # Register all component definitions
286
+ for comp_name, comp_def_data in component_definitions.items():
287
+ # Validate required keys and gracefully handle optional ones.
288
+ name = comp_def_data.get("name")
289
+ if not name:
290
+ raise ValueError(
291
+ f"Component definition for key '{comp_name}' is missing required 'name' field"
292
+ )
293
+
294
+ definition = BidiComponentDefinition(
295
+ name=name,
296
+ js=comp_def_data.get("js"),
297
+ css=comp_def_data.get("css"),
298
+ html=comp_def_data.get("html"),
299
+ css_asset_relative_path=comp_def_data.get(
300
+ "css_asset_relative_path"
301
+ ),
302
+ js_asset_relative_path=comp_def_data.get("js_asset_relative_path"),
303
+ )
304
+ self._components[comp_name] = definition
305
+ _LOGGER.debug(
306
+ "Registered component %s from processed definitions", comp_name
307
+ )
308
+
309
+ def register(self, definition: BidiComponentDefinition) -> None:
310
+ """Register or overwrite a component definition by name.
311
+
312
+ This method is the primary entry point for adding a component to the
313
+ registry. It is used when a component is first declared via the public
314
+ API (e.g., ``st.components.v2.component``).
315
+
316
+ If a component with the same name already exists (e.g., a placeholder
317
+ from a manifest scan), it is overwritten. A warning is logged if the
318
+ new definition differs from the old one to alert developers of
319
+ potential conflicts.
320
+
321
+ Parameters
322
+ ----------
323
+ definition : BidiComponentDefinition
324
+ The component definition to store.
325
+ """
326
+
327
+ # Register the definition
328
+ with self._lock:
329
+ name = definition.name
330
+ if name in self._components:
331
+ existing_definition = self._components[name]
332
+ if existing_definition != definition:
333
+ _LOGGER.warning(
334
+ "Component %s is already registered. Overwriting "
335
+ "previous definition. This may lead to unexpected behavior "
336
+ "if different modules register the same component name with "
337
+ "different definitions.",
338
+ name,
339
+ )
340
+ self._components[name] = definition
341
+ _LOGGER.debug("Registered component %s", name)
342
+
343
+ def get(self, name: str) -> BidiComponentDefinition | None:
344
+ """Return a component definition by name, or ``None`` if not found.
345
+
346
+ Parameters
347
+ ----------
348
+ name : str
349
+ Component name to retrieve.
350
+
351
+ Returns
352
+ -------
353
+ BidiComponentDefinition or None
354
+ The component definition if present, otherwise ``None``.
355
+ """
356
+ with self._lock:
357
+ return self._components.get(name)
358
+
359
+ def unregister(self, name: str) -> None:
360
+ """Remove a component definition from the registry.
361
+
362
+ Primarily useful for tests and dynamic scenarios.
363
+
364
+ Parameters
365
+ ----------
366
+ name : str
367
+ Component name to unregister.
368
+ """
369
+ with self._lock:
370
+ if name in self._components:
371
+ del self._components[name]
372
+ _LOGGER.debug("Unregistered component %s", name)
373
+
374
+ def clear(self) -> None:
375
+ """Clear all component definitions from the registry."""
376
+ with self._lock:
377
+ self._components.clear()
378
+ _LOGGER.debug("Cleared all components from registry")
379
+
380
+ def update_component(self, definition: BidiComponentDefinition) -> None:
381
+ """Update (replace) a stored component definition by name.
382
+
383
+ This method provides a stricter way to update a component definition
384
+ and is used for internal processes like file-watcher updates. Unlike
385
+ ``register``, it will raise an error if the component is not already
386
+ present in the registry.
387
+
388
+ This ensures that background processes can only modify components that
389
+ have been explicitly defined in the current session, preventing race
390
+ conditions or unexpected behavior where a file-watcher event might try
391
+ to update a component that has since been unregistered.
392
+
393
+ Callers must supply a fully validated :class:`BidiComponentDefinition`.
394
+ The registry replaces the stored definition under ``definition.name`` in
395
+ a thread-safe manner.
396
+
397
+ Parameters
398
+ ----------
399
+ definition : BidiComponentDefinition
400
+ The fully-resolved component definition to store.
401
+ """
402
+ with self._lock:
403
+ name = definition.name
404
+ if name not in self._components:
405
+ raise StreamlitComponentRegistryError(
406
+ f"Cannot update unregistered component: {name}"
407
+ )
408
+ self._components[name] = definition
409
+ _LOGGER.debug("Updated component definition for %s", name)