streamlit-nightly 1.52.3.dev20260106__py3-none-any.whl → 1.52.3.dev20260107__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/components/v2/__init__.py +33 -16
- streamlit/components/v2/bidi_component/main.py +0 -8
- streamlit/components/v2/types.py +13 -20
- streamlit/config.py +10 -0
- streamlit/elements/widgets/number_input.py +27 -24
- streamlit/errors.py +0 -12
- streamlit/runtime/context.py +22 -30
- streamlit/runtime/session_manager.py +35 -2
- streamlit/web/server/browser_websocket_handler.py +48 -5
- streamlit/web/server/server.py +43 -8
- streamlit/web/server/starlette/__init__.py +9 -0
- streamlit/web/server/starlette/starlette_routes.py +4 -0
- streamlit/web/server/starlette/starlette_server.py +496 -0
- streamlit/web/server/starlette/starlette_websocket.py +39 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/RECORD +20 -19
- {streamlit_nightly-1.52.3.dev20260106.data → streamlit_nightly-1.52.3.dev20260107.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.52.3.dev20260106.dist-info → streamlit_nightly-1.52.3.dev20260107.dist-info}/top_level.txt +0 -0
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
from typing import TYPE_CHECKING, Any
|
|
20
20
|
|
|
21
|
+
from streamlit import deprecation_util
|
|
21
22
|
from streamlit.components.v2.component_definition_resolver import (
|
|
22
23
|
build_definition_with_validation,
|
|
23
24
|
)
|
|
@@ -106,6 +107,7 @@ def _create_component_callable(
|
|
|
106
107
|
html: str | None = None,
|
|
107
108
|
css: str | None = None,
|
|
108
109
|
js: str | None = None,
|
|
110
|
+
isolate_styles: bool = True,
|
|
109
111
|
) -> Callable[..., Any]:
|
|
110
112
|
"""Create a component callable, handling both lookup and registration cases.
|
|
111
113
|
|
|
@@ -121,6 +123,9 @@ def _create_component_callable(
|
|
|
121
123
|
js : str | None
|
|
122
124
|
Inline JavaScript (string) or a string path/glob to a file under
|
|
123
125
|
``asset_dir``; see :func:`_register_component` for path validation semantics.
|
|
126
|
+
isolate_styles : bool
|
|
127
|
+
Whether to sandbox the component's styles in a shadow root.
|
|
128
|
+
Defaults to True.
|
|
124
129
|
|
|
125
130
|
Returns
|
|
126
131
|
-------
|
|
@@ -143,7 +148,6 @@ def _create_component_callable(
|
|
|
143
148
|
default: dict[str, Any] | None = None,
|
|
144
149
|
width: Width = "stretch",
|
|
145
150
|
height: Height = "content",
|
|
146
|
-
isolate_styles: bool = True,
|
|
147
151
|
**on_callbacks: WidgetCallback | None,
|
|
148
152
|
) -> BidiComponentResult:
|
|
149
153
|
"""Mount the component.
|
|
@@ -161,9 +165,6 @@ def _create_component_callable(
|
|
|
161
165
|
The width of the component.
|
|
162
166
|
height : Height
|
|
163
167
|
The height of the component.
|
|
164
|
-
isolate_styles : bool
|
|
165
|
-
Whether to sandbox the component styles in a shadow-root. Defaults to
|
|
166
|
-
True.
|
|
167
168
|
**on_callbacks : WidgetCallback
|
|
168
169
|
Callback functions for handling component events. Use pattern
|
|
169
170
|
on_{state_name}_change (e.g., on_click_change, on_value_change).
|
|
@@ -175,6 +176,20 @@ def _create_component_callable(
|
|
|
175
176
|
"""
|
|
176
177
|
import streamlit as st
|
|
177
178
|
|
|
179
|
+
# Backwards-tolerant: old code may still pass isolate_styles at mount
|
|
180
|
+
# time. Since isolate_styles is now configured on component(), ignore it
|
|
181
|
+
# here to prevent a runtime error.
|
|
182
|
+
if "isolate_styles" in on_callbacks:
|
|
183
|
+
on_callbacks.pop("isolate_styles", None)
|
|
184
|
+
|
|
185
|
+
deprecation_util.show_deprecation_warning(
|
|
186
|
+
"Passing `isolate_styles` when mounting a component created via "
|
|
187
|
+
"`st.components.v2.component` is deprecated and will be ignored. "
|
|
188
|
+
"Set `isolate_styles` when creating the component instead "
|
|
189
|
+
"(`st.components.v2.component(..., isolate_styles=...)`).",
|
|
190
|
+
show_in_browser=False,
|
|
191
|
+
)
|
|
192
|
+
|
|
178
193
|
return st._bidi_component(
|
|
179
194
|
component_key,
|
|
180
195
|
key=key,
|
|
@@ -186,10 +201,6 @@ def _create_component_callable(
|
|
|
186
201
|
**on_callbacks,
|
|
187
202
|
)
|
|
188
203
|
|
|
189
|
-
# Ensure the function remains compatible with the shared public callable type.
|
|
190
|
-
# Static type assertion to ensure the callable matches the shared signature
|
|
191
|
-
_typed_check_mount_component: BidiComponentCallable = _mount_component
|
|
192
|
-
|
|
193
204
|
return _mount_component
|
|
194
205
|
|
|
195
206
|
|
|
@@ -199,11 +210,13 @@ def component(
|
|
|
199
210
|
html: str | None = None,
|
|
200
211
|
css: str | None = None,
|
|
201
212
|
js: str | None = None,
|
|
213
|
+
isolate_styles: bool = True,
|
|
202
214
|
) -> BidiComponentCallable:
|
|
203
215
|
'''Register an ``st.components.v2`` component and return a callable to mount it.
|
|
204
216
|
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
Components may provide any combination of HTML, CSS, and JavaScript. If none
|
|
218
|
+
are provided, the component will render as an empty element without raising
|
|
219
|
+
an error.
|
|
207
220
|
|
|
208
221
|
If your component is defined in an installed package, you can declare an
|
|
209
222
|
asset directory (``asset_dir``) through ``pyproject.toml`` files in the
|
|
@@ -250,9 +263,6 @@ def component(
|
|
|
250
263
|
element and populate it via JavaScript. Alternatively, you can append
|
|
251
264
|
a new element to the parent. For more information, see Example 2.
|
|
252
265
|
|
|
253
|
-
``html`` and ``js`` can't both be ``None``. At least one of them must
|
|
254
|
-
be provided.
|
|
255
|
-
|
|
256
266
|
css : str or None
|
|
257
267
|
Inline CSS. This can be one of the following strings:
|
|
258
268
|
|
|
@@ -267,8 +277,13 @@ def component(
|
|
|
267
277
|
- A path or glob to a JS file, relative to the component's
|
|
268
278
|
asset directory.
|
|
269
279
|
|
|
270
|
-
|
|
271
|
-
|
|
280
|
+
isolate_styles : bool
|
|
281
|
+
Whether to sandbox the component styles in a shadow root. If this is
|
|
282
|
+
``True`` (default), the component's HTML is mounted inside a shadow DOM
|
|
283
|
+
and, in your component's JavaScript, ``parentElement`` returns a
|
|
284
|
+
``ShadowRoot``. If this is ``False``, the component's HTML is mounted
|
|
285
|
+
directly into the app's DOM tree, and ``parentElement`` returns a
|
|
286
|
+
regular ``HTMLElement``.
|
|
272
287
|
|
|
273
288
|
Returns
|
|
274
289
|
-------
|
|
@@ -506,7 +521,9 @@ def component(
|
|
|
506
521
|
height: 250px
|
|
507
522
|
|
|
508
523
|
'''
|
|
509
|
-
return _create_component_callable(
|
|
524
|
+
return _create_component_callable(
|
|
525
|
+
name, html=html, css=css, js=js, isolate_styles=isolate_styles
|
|
526
|
+
)
|
|
510
527
|
|
|
511
528
|
|
|
512
529
|
__all__ = [
|
|
@@ -50,7 +50,6 @@ from streamlit.errors import (
|
|
|
50
50
|
BidiComponentInvalidCallbackNameError,
|
|
51
51
|
BidiComponentInvalidDefaultKeyError,
|
|
52
52
|
BidiComponentInvalidIdError,
|
|
53
|
-
BidiComponentMissingContentError,
|
|
54
53
|
BidiComponentUnserializableDataError,
|
|
55
54
|
)
|
|
56
55
|
from streamlit.proto.ArrowData_pb2 import ArrowData as ArrowDataProto
|
|
@@ -349,13 +348,6 @@ class BidiComponentMixin:
|
|
|
349
348
|
if component_def is None:
|
|
350
349
|
raise ValueError(f"Component '{component_name}' is not registered")
|
|
351
350
|
|
|
352
|
-
# Validate that the component has the required content
|
|
353
|
-
has_js = bool(component_def.js_content or component_def.js_url)
|
|
354
|
-
has_html = bool(component_def.html_content)
|
|
355
|
-
|
|
356
|
-
if not has_js and not has_html:
|
|
357
|
-
raise BidiComponentMissingContentError(component_name)
|
|
358
|
-
|
|
359
351
|
# ------------------------------------------------------------------
|
|
360
352
|
# 1. Parse user-supplied callbacks
|
|
361
353
|
# ------------------------------------------------------------------
|
streamlit/components/v2/types.py
CHANGED
|
@@ -22,6 +22,10 @@ authoring wrappers/utilities around `st.components.v2.component`.
|
|
|
22
22
|
The goal is to keep the public argument surface documented in one place and
|
|
23
23
|
reusable across both the user-facing factory in `components/v2/__init__.py`
|
|
24
24
|
and the internal implementation in `components/v2/bidi_component/main.py`.
|
|
25
|
+
|
|
26
|
+
Component definitions may provide any combination of HTML, CSS, and
|
|
27
|
+
JavaScript. If none are supplied, the component renders as an empty
|
|
28
|
+
placeholder without raising an error.
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
31
|
from __future__ import annotations
|
|
@@ -137,14 +141,6 @@ class BidiComponentCallable(Protocol):
|
|
|
137
141
|
You are responsible for ensuring the component's inner HTML content is
|
|
138
142
|
responsive to the ``<div>`` wrapper.
|
|
139
143
|
|
|
140
|
-
isolate_styles : bool
|
|
141
|
-
Whether to sandbox the component styles in a shadow root. If this is
|
|
142
|
-
``True`` (default), the component's HTML is mounted inside a shadow DOM
|
|
143
|
-
and, in your component's JavaScript, ``parentElement`` returns a
|
|
144
|
-
``ShadowRoot``. If this is ``False``, the component's HTML is mounted
|
|
145
|
-
directly into the app's DOM tree, and ``parentElement`` returns a
|
|
146
|
-
regular ``HTMLElement``.
|
|
147
|
-
|
|
148
144
|
**callbacks : Callable or None
|
|
149
145
|
Callbacks with the naming pattern ``on_<key>_change`` for each state and
|
|
150
146
|
trigger key. For example, if your component has a state key of
|
|
@@ -251,11 +247,12 @@ class BidiComponentCallable(Protocol):
|
|
|
251
247
|
|
|
252
248
|
**Example 2: Add Tailwind CSS to a component**
|
|
253
249
|
|
|
254
|
-
You can use the ``isolate_styles`` parameter
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
You can use the ``isolate_styles`` parameter in
|
|
251
|
+
``st.components.v2.component`` to disable shadow DOM isolation and apply
|
|
252
|
+
global styles like Tailwind CSS to your component. The following example
|
|
253
|
+
creates a simple button styled with Tailwind CSS. This example also
|
|
254
|
+
demonstrates using different keys to mount multiple instances of the same
|
|
255
|
+
component in one app.
|
|
259
256
|
|
|
260
257
|
.. code-block:: python
|
|
261
258
|
|
|
@@ -285,15 +282,12 @@ class BidiComponentCallable(Protocol):
|
|
|
285
282
|
"my_tailwind_button",
|
|
286
283
|
html=HTML,
|
|
287
284
|
js=JS,
|
|
285
|
+
isolate_styles=False,
|
|
288
286
|
)
|
|
289
|
-
result_1 = my_component(
|
|
290
|
-
isolate_styles=False, on_clicked_change=lambda: None, key="one"
|
|
291
|
-
)
|
|
287
|
+
result_1 = my_component(on_clicked_change=lambda: None, key="one")
|
|
292
288
|
result_1
|
|
293
289
|
|
|
294
|
-
result_2 = my_component(
|
|
295
|
-
isolate_styles=False, on_clicked_change=lambda: None, key="two"
|
|
296
|
-
)
|
|
290
|
+
result_2 = my_component(on_clicked_change=lambda: None, key="two")
|
|
297
291
|
result_2
|
|
298
292
|
|
|
299
293
|
.. output::
|
|
@@ -310,7 +304,6 @@ class BidiComponentCallable(Protocol):
|
|
|
310
304
|
default: BidiComponentDefaults = None,
|
|
311
305
|
width: Width = "stretch",
|
|
312
306
|
height: Height = "content",
|
|
313
|
-
isolate_styles: ComponentIsolateStyles = True,
|
|
314
307
|
**on_callbacks: WidgetCallback | None,
|
|
315
308
|
) -> BidiComponentResult: ...
|
|
316
309
|
|
streamlit/config.py
CHANGED
|
@@ -1003,6 +1003,16 @@ _create_option(
|
|
|
1003
1003
|
visibility="hidden",
|
|
1004
1004
|
)
|
|
1005
1005
|
|
|
1006
|
+
_create_option(
|
|
1007
|
+
"server.useStarlette",
|
|
1008
|
+
description="""
|
|
1009
|
+
Enable the experimental Starlette-based server implementation instead of
|
|
1010
|
+
Tornado. This is an experimental feature and may be removed in the future.
|
|
1011
|
+
""",
|
|
1012
|
+
default_val=False,
|
|
1013
|
+
type_=bool,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1006
1016
|
# Config Section: Browser #
|
|
1007
1017
|
|
|
1008
1018
|
_create_section("browser", "Configuration of non-UI browser options.")
|
|
@@ -452,9 +452,7 @@ class NumberInputMixin:
|
|
|
452
452
|
element_id = compute_and_register_element_id(
|
|
453
453
|
"number_input",
|
|
454
454
|
user_key=key,
|
|
455
|
-
|
|
456
|
-
# that might invalidate the current widget state.
|
|
457
|
-
key_as_main_identity={"min_value", "max_value", "step"},
|
|
455
|
+
key_as_main_identity=True,
|
|
458
456
|
dg=self.dg,
|
|
459
457
|
label=label,
|
|
460
458
|
min_value=min_value,
|
|
@@ -630,26 +628,31 @@ class NumberInputMixin:
|
|
|
630
628
|
value_type="double_value",
|
|
631
629
|
)
|
|
632
630
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
631
|
+
# Validate the current value against the new min/max bounds.
|
|
632
|
+
# If the value is no longer valid (outside bounds), reset to default.
|
|
633
|
+
# This handles the case where min_value/max_value change dynamically and the
|
|
634
|
+
# previously entered value is no longer within bounds.
|
|
635
|
+
current_value = widget_state.value
|
|
636
|
+
value_needs_reset = False
|
|
637
|
+
|
|
638
|
+
# Check if the current value is outside the new bounds.
|
|
639
|
+
if current_value is not None and (
|
|
640
|
+
(number_input_proto.has_min and current_value < number_input_proto.min)
|
|
641
|
+
or (number_input_proto.has_max and current_value > number_input_proto.max)
|
|
642
|
+
):
|
|
643
|
+
# Value is outside new bounds - reset to default.
|
|
644
|
+
value_needs_reset = True
|
|
645
|
+
current_value = value
|
|
646
|
+
|
|
647
|
+
# Update session_state so subsequent accesses in this run
|
|
648
|
+
# return the corrected value. Use reset_state_value to avoid
|
|
649
|
+
# the "cannot be modified after widget instantiated" error.
|
|
650
|
+
if key is not None:
|
|
651
|
+
get_session_state().reset_state_value(key, current_value)
|
|
652
|
+
|
|
653
|
+
if value_needs_reset or widget_state.value_changed:
|
|
654
|
+
if current_value is not None:
|
|
655
|
+
number_input_proto.value = current_value
|
|
653
656
|
number_input_proto.set_value = True
|
|
654
657
|
|
|
655
658
|
validate_width(width)
|
|
@@ -658,7 +661,7 @@ class NumberInputMixin:
|
|
|
658
661
|
self.dg._enqueue(
|
|
659
662
|
"number_input", number_input_proto, layout_config=layout_config
|
|
660
663
|
)
|
|
661
|
-
return
|
|
664
|
+
return current_value
|
|
662
665
|
|
|
663
666
|
@property
|
|
664
667
|
def dg(self) -> DeltaGenerator:
|
streamlit/errors.py
CHANGED
|
@@ -476,18 +476,6 @@ class BidiComponentInvalidIdError(LocalizableStreamlitException):
|
|
|
476
476
|
)
|
|
477
477
|
|
|
478
478
|
|
|
479
|
-
class BidiComponentMissingContentError(LocalizableStreamlitException):
|
|
480
|
-
"""Exception raised when a component is missing required content."""
|
|
481
|
-
|
|
482
|
-
def __init__(self, component_name: str) -> None:
|
|
483
|
-
super().__init__(
|
|
484
|
-
"Component `{component_name}` must have either JavaScript content "
|
|
485
|
-
"(`js_content` or `js_url`) or HTML content (`html_content`), or both. "
|
|
486
|
-
"Please ensure the component definition includes at least one of these.",
|
|
487
|
-
component_name=component_name,
|
|
488
|
-
)
|
|
489
|
-
|
|
490
|
-
|
|
491
479
|
class BidiComponentInvalidCallbackNameError(LocalizableStreamlitException):
|
|
492
480
|
"""Exception raised when a callback with an invalid name is provided."""
|
|
493
481
|
|
streamlit/runtime/context.py
CHANGED
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from collections.abc import Iterable, Iterator, Mapping
|
|
18
18
|
from functools import lru_cache
|
|
19
19
|
from types import MappingProxyType
|
|
20
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
21
21
|
|
|
22
22
|
from streamlit import runtime
|
|
23
23
|
from streamlit.runtime.context_util import maybe_add_page_path, maybe_trim_page_path
|
|
@@ -28,11 +28,18 @@ from streamlit.util import AttributeDictionary
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
29
|
from http.cookies import Morsel
|
|
30
30
|
|
|
31
|
-
from tornado.httputil import HTTPHeaders
|
|
32
|
-
from tornado.web import RequestHandler
|
|
31
|
+
from tornado.httputil import HTTPHeaders
|
|
33
32
|
|
|
33
|
+
from streamlit.runtime.session_manager import ClientContext
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
def _get_client_context() -> ClientContext | None:
|
|
37
|
+
"""Get the ClientContext for the current session.
|
|
38
|
+
|
|
39
|
+
Returns the client context from the session client if available,
|
|
40
|
+
or None if not available (e.g., no active session or the session
|
|
41
|
+
client doesn't provide a client context).
|
|
42
|
+
"""
|
|
36
43
|
ctx = get_script_run_ctx()
|
|
37
44
|
if ctx is None:
|
|
38
45
|
return None
|
|
@@ -41,17 +48,7 @@ def _get_request() -> HTTPServerRequest | None:
|
|
|
41
48
|
if session_client is None:
|
|
42
49
|
return None
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
# BrowserWebSocketHandler (which is True for the Streamlit open-source
|
|
46
|
-
# implementation). For any other implementation, we return None.
|
|
47
|
-
# We are not using `type_util.is_type` here to avoid circular import.
|
|
48
|
-
if (
|
|
49
|
-
f"{type(session_client).__module__}.{type(session_client).__qualname__}"
|
|
50
|
-
!= "streamlit.web.server.browser_websocket_handler.BrowserWebSocketHandler"
|
|
51
|
-
):
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
return cast("RequestHandler", session_client).request
|
|
51
|
+
return session_client.client_context
|
|
55
52
|
|
|
56
53
|
|
|
57
54
|
@lru_cache
|
|
@@ -195,12 +192,12 @@ class ContextProxy:
|
|
|
195
192
|
"""
|
|
196
193
|
# We have a docstring in line above as one-liner, to have a correct docstring
|
|
197
194
|
# in the st.write(st,context) call.
|
|
198
|
-
|
|
195
|
+
client_context = _get_client_context()
|
|
199
196
|
|
|
200
|
-
if
|
|
197
|
+
if client_context is None:
|
|
201
198
|
return StreamlitHeaders({})
|
|
202
199
|
|
|
203
|
-
return StreamlitHeaders
|
|
200
|
+
return StreamlitHeaders(client_context.headers)
|
|
204
201
|
|
|
205
202
|
@property
|
|
206
203
|
@gather_metrics("context.cookies")
|
|
@@ -228,13 +225,12 @@ class ContextProxy:
|
|
|
228
225
|
"""
|
|
229
226
|
# We have a docstring in line above as one-liner, to have a correct docstring
|
|
230
227
|
# in the st.write(st,context) call.
|
|
231
|
-
|
|
228
|
+
client_context = _get_client_context()
|
|
232
229
|
|
|
233
|
-
if
|
|
230
|
+
if client_context is None:
|
|
234
231
|
return StreamlitCookies({})
|
|
235
232
|
|
|
236
|
-
|
|
237
|
-
return StreamlitCookies.from_tornado_cookies(cookies)
|
|
233
|
+
return StreamlitCookies(client_context.cookies)
|
|
238
234
|
|
|
239
235
|
@property
|
|
240
236
|
@gather_metrics("context.theme")
|
|
@@ -399,11 +395,7 @@ class ContextProxy:
|
|
|
399
395
|
This should not be used for security measures because it can easily be
|
|
400
396
|
spoofed. When a user accesses the app through ``localhost``, the IP
|
|
401
397
|
address is ``None``. Otherwise, the IP address is determined from the
|
|
402
|
-
|
|
403
|
-
IPv4 or IPv6 address.
|
|
404
|
-
|
|
405
|
-
.. |remote_ip| replace:: ``remote_ip``
|
|
406
|
-
.. _remote_ip: https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest.remote_ip
|
|
398
|
+
WebSocket connection and may be an IPv4 or IPv6 address.
|
|
407
399
|
|
|
408
400
|
Example
|
|
409
401
|
-------
|
|
@@ -421,9 +413,9 @@ class ContextProxy:
|
|
|
421
413
|
>>> else:
|
|
422
414
|
>>> st.error("This should not happen.")
|
|
423
415
|
"""
|
|
424
|
-
|
|
425
|
-
if
|
|
426
|
-
remote_ip =
|
|
416
|
+
client_context = _get_client_context()
|
|
417
|
+
if client_context is not None:
|
|
418
|
+
remote_ip = client_context.remote_ip
|
|
427
419
|
if remote_ip in {"::1", "127.0.0.1"}:
|
|
428
420
|
return None
|
|
429
421
|
return remote_ip
|
|
@@ -16,10 +16,10 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from abc import abstractmethod
|
|
18
18
|
from dataclasses import dataclass
|
|
19
|
-
from typing import TYPE_CHECKING, Protocol, cast
|
|
19
|
+
from typing import TYPE_CHECKING, Protocol, cast, runtime_checkable
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
-
from collections.abc import Callable
|
|
22
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
23
23
|
|
|
24
24
|
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
|
25
25
|
from streamlit.runtime.app_session import AppSession
|
|
@@ -33,6 +33,31 @@ class SessionClientDisconnectedError(Exception):
|
|
|
33
33
|
"""Raised by operations on a disconnected SessionClient."""
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
@runtime_checkable
|
|
37
|
+
class ClientContext(Protocol):
|
|
38
|
+
"""Framework-agnostic context for the client WebSocket connection.
|
|
39
|
+
|
|
40
|
+
This protocol abstracts away framework-specific request types (Tornado/Starlette)
|
|
41
|
+
to provide a consistent interface for accessing headers, cookies, and client info
|
|
42
|
+
from the initial WebSocket handshake.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def headers(self) -> Iterable[tuple[str, str]]:
|
|
47
|
+
"""All headers as (name, value) tuples. Headers may be repeated."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def cookies(self) -> Mapping[str, str]:
|
|
52
|
+
"""Cookies as a name-to-value mapping."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def remote_ip(self) -> str | None:
|
|
57
|
+
"""The remote IP address of the client, or None if unavailable."""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
|
|
36
61
|
class SessionClient(Protocol):
|
|
37
62
|
"""Interface for sending data to a session's client."""
|
|
38
63
|
|
|
@@ -45,6 +70,14 @@ class SessionClient(Protocol):
|
|
|
45
70
|
"""
|
|
46
71
|
raise NotImplementedError
|
|
47
72
|
|
|
73
|
+
@property
|
|
74
|
+
def client_context(self) -> ClientContext | None:
|
|
75
|
+
"""The client's connection context (headers, cookies, IP).
|
|
76
|
+
|
|
77
|
+
Returns None if request context information is not available.
|
|
78
|
+
"""
|
|
79
|
+
return None
|
|
80
|
+
|
|
48
81
|
|
|
49
82
|
@dataclass
|
|
50
83
|
class ActiveSessionInfo:
|
|
@@ -19,10 +19,6 @@ import json
|
|
|
19
19
|
from typing import TYPE_CHECKING, Any, Final
|
|
20
20
|
from urllib.parse import urlparse
|
|
21
21
|
|
|
22
|
-
import tornado.concurrent
|
|
23
|
-
import tornado.locks
|
|
24
|
-
import tornado.netutil
|
|
25
|
-
import tornado.web
|
|
26
22
|
import tornado.websocket
|
|
27
23
|
from tornado.escape import utf8
|
|
28
24
|
from tornado.websocket import WebSocketHandler
|
|
@@ -33,6 +29,7 @@ from streamlit.logger import get_logger
|
|
|
33
29
|
from streamlit.proto.BackMsg_pb2 import BackMsg
|
|
34
30
|
from streamlit.runtime import Runtime, SessionClient, SessionClientDisconnectedError
|
|
35
31
|
from streamlit.runtime.runtime_util import serialize_forward_msg
|
|
32
|
+
from streamlit.runtime.session_manager import ClientContext
|
|
36
33
|
from streamlit.web.server.server_util import (
|
|
37
34
|
AUTH_COOKIE_NAME,
|
|
38
35
|
TOKENS_COOKIE_NAME,
|
|
@@ -41,13 +38,46 @@ from streamlit.web.server.server_util import (
|
|
|
41
38
|
)
|
|
42
39
|
|
|
43
40
|
if TYPE_CHECKING:
|
|
44
|
-
from collections.abc import Awaitable
|
|
41
|
+
from collections.abc import Awaitable, Iterable, Mapping
|
|
42
|
+
|
|
43
|
+
from tornado.httputil import HTTPServerRequest
|
|
45
44
|
|
|
46
45
|
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
|
47
46
|
|
|
48
47
|
_LOGGER: Final = get_logger(__name__)
|
|
49
48
|
|
|
50
49
|
|
|
50
|
+
class TornadoClientContext(ClientContext):
|
|
51
|
+
"""Tornado-specific implementation of ClientContext.
|
|
52
|
+
|
|
53
|
+
Captures headers, cookies, and client info from the initial WebSocket handshake.
|
|
54
|
+
Values are cached at construction time since they represent the initial request
|
|
55
|
+
context and should not change during the connection lifetime.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, tornado_request: HTTPServerRequest) -> None:
|
|
59
|
+
self._headers: list[tuple[str, str]] = list(tornado_request.headers.get_all())
|
|
60
|
+
self._cookies: dict[str, str] = {
|
|
61
|
+
k: m.value for k, m in tornado_request.cookies.items()
|
|
62
|
+
}
|
|
63
|
+
self._remote_ip: str | None = tornado_request.remote_ip
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def headers(self) -> Iterable[tuple[str, str]]:
|
|
67
|
+
"""All headers as (name, value) tuples."""
|
|
68
|
+
return self._headers
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def cookies(self) -> Mapping[str, str]:
|
|
72
|
+
"""Cookies as a name-to-value mapping."""
|
|
73
|
+
return self._cookies
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def remote_ip(self) -> str | None:
|
|
77
|
+
"""The client's remote IP address."""
|
|
78
|
+
return self._remote_ip
|
|
79
|
+
|
|
80
|
+
|
|
51
81
|
class BrowserWebSocketHandler(WebSocketHandler, SessionClient):
|
|
52
82
|
"""Handles a WebSocket connection from the browser."""
|
|
53
83
|
|
|
@@ -143,6 +173,19 @@ class BrowserWebSocketHandler(WebSocketHandler, SessionClient):
|
|
|
143
173
|
except tornado.websocket.WebSocketClosedError as e:
|
|
144
174
|
raise SessionClientDisconnectedError from e
|
|
145
175
|
|
|
176
|
+
@property
|
|
177
|
+
def client_context(self) -> ClientContext:
|
|
178
|
+
"""Return the client's connection context.
|
|
179
|
+
|
|
180
|
+
The context is cached on first access to avoid repeatedly
|
|
181
|
+
constructing a new TornadoClientContext instance.
|
|
182
|
+
"""
|
|
183
|
+
context = getattr(self, "_client_context", None)
|
|
184
|
+
if context is None:
|
|
185
|
+
context = TornadoClientContext(self.request)
|
|
186
|
+
self._client_context = context
|
|
187
|
+
return context
|
|
188
|
+
|
|
146
189
|
def select_subprotocol(self, subprotocols: list[str]) -> str | None:
|
|
147
190
|
"""Return the first subprotocol in the given list.
|
|
148
191
|
|