streamlit-nightly 1.32.3.dev20240325__py2.py3-none-any.whl → 1.32.3.dev20240328__py2.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 -2
- streamlit/components/v1/__init__.py +3 -17
- streamlit/components/v1/{custom_component.py → components.py} +159 -11
- streamlit/delta_generator.py +3 -0
- streamlit/elements/media.py +52 -4
- streamlit/elements/widgets/time_widgets.py +5 -21
- streamlit/errors.py +0 -6
- streamlit/proto/AutoRerun_pb2.py +25 -0
- streamlit/proto/AutoRerun_pb2.pyi +48 -0
- streamlit/proto/ClientState_pb2.py +3 -3
- streamlit/proto/ClientState_pb2.pyi +4 -1
- streamlit/proto/Delta_pb2.py +2 -2
- streamlit/proto/Delta_pb2.pyi +4 -1
- streamlit/proto/ForwardMsg_pb2.py +10 -9
- streamlit/proto/ForwardMsg_pb2.pyi +12 -3
- streamlit/proto/NewSession_pb2.py +24 -24
- streamlit/proto/NewSession_pb2.pyi +8 -1
- streamlit/proto/PageProfile_pb2.py +6 -6
- streamlit/proto/PageProfile_pb2.pyi +4 -1
- streamlit/runtime/app_session.py +67 -24
- streamlit/runtime/caching/cache_data_api.py +3 -3
- streamlit/runtime/caching/cache_errors.py +0 -11
- streamlit/runtime/caching/cache_resource_api.py +2 -2
- streamlit/runtime/caching/cache_utils.py +1 -43
- streamlit/runtime/fragment.py +239 -0
- streamlit/runtime/metrics_util.py +17 -9
- streamlit/runtime/runtime.py +6 -12
- streamlit/runtime/runtime_util.py +54 -2
- streamlit/runtime/scriptrunner/script_requests.py +53 -37
- streamlit/runtime/scriptrunner/script_run_context.py +15 -2
- streamlit/runtime/scriptrunner/script_runner.py +63 -14
- streamlit/runtime/state/common.py +2 -0
- streamlit/runtime/state/session_state.py +51 -7
- streamlit/runtime/state/widgets.py +10 -2
- streamlit/static/asset-manifest.json +19 -19
- streamlit/static/index.html +1 -1
- streamlit/static/static/js/1074.73973756.chunk.js +1 -0
- streamlit/static/static/js/1451.3b0a3e31.chunk.js +1 -0
- streamlit/static/static/js/1792.b8efa879.chunk.js +1 -0
- streamlit/static/static/js/{3092.3d4df25e.chunk.js → 3092.ad569cc8.chunk.js} +1 -1
- streamlit/static/static/js/3513.e3e7300a.chunk.js +1 -0
- streamlit/static/static/js/4177.69f9f18d.chunk.js +1 -0
- streamlit/static/static/js/4319.a6745434.chunk.js +1 -0
- streamlit/static/static/js/{4477.2555c11a.chunk.js → 4477.e10e4373.chunk.js} +1 -1
- streamlit/static/static/js/{4666.99f3abc3.chunk.js → 4666.b694c5a9.chunk.js} +1 -1
- streamlit/static/static/js/5106.44f0ff51.chunk.js +1 -0
- streamlit/static/static/js/5379.6571574f.chunk.js +1 -0
- streamlit/static/static/js/6013.8e80e091.chunk.js +1 -0
- streamlit/static/static/js/6718.802da17e.chunk.js +1 -0
- streamlit/static/static/js/7175.be4076bc.chunk.js +1 -0
- streamlit/static/static/js/{7602.f0420392.chunk.js → 7602.6175e969.chunk.js} +1 -1
- streamlit/static/static/js/{8492.e6dab83f.chunk.js → 8492.f56c9d4c.chunk.js} +1 -1
- streamlit/static/static/js/8691.9ccf7f89.chunk.js +1 -0
- streamlit/static/static/js/main.722453f0.js +2 -0
- streamlit/testing/v1/local_script_runner.py +2 -0
- streamlit/time_util.py +88 -0
- streamlit/watcher/local_sources_watcher.py +2 -1
- streamlit/web/server/component_request_handler.py +2 -2
- streamlit/web/server/server.py +2 -1
- {streamlit_nightly-1.32.3.dev20240325.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.32.3.dev20240325.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/RECORD +66 -68
- streamlit/components/lib/__init__.py +0 -13
- streamlit/components/lib/local_component_registry.py +0 -82
- streamlit/components/types/__init__.py +0 -13
- streamlit/components/types/base_component_registry.py +0 -98
- streamlit/components/types/base_custom_component.py +0 -137
- streamlit/components/v1/component_registry.py +0 -103
- streamlit/static/static/js/1074.71719df6.chunk.js +0 -1
- streamlit/static/static/js/1451.e3be1711.chunk.js +0 -1
- streamlit/static/static/js/1792.16c16498.chunk.js +0 -1
- streamlit/static/static/js/3513.57cff89c.chunk.js +0 -1
- streamlit/static/static/js/4177.ab9a7aa1.chunk.js +0 -1
- streamlit/static/static/js/4319.213fc321.chunk.js +0 -1
- streamlit/static/static/js/5106.22187bfc.chunk.js +0 -1
- streamlit/static/static/js/5379.e466522d.chunk.js +0 -1
- streamlit/static/static/js/6013.75c92264.chunk.js +0 -1
- streamlit/static/static/js/6718.97945fc6.chunk.js +0 -1
- streamlit/static/static/js/7175.8c1b4d38.chunk.js +0 -1
- streamlit/static/static/js/8691.24a5792f.chunk.js +0 -1
- streamlit/static/static/js/main.7fde7092.js +0 -2
- /streamlit/static/static/js/{main.7fde7092.js.LICENSE.txt → main.722453f0.js.LICENSE.txt} +0 -0
- {streamlit_nightly-1.32.3.dev20240325.data → streamlit_nightly-1.32.3.dev20240328.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.32.3.dev20240325.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.32.3.dev20240325.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.32.3.dev20240325.dist-info → streamlit_nightly-1.32.3.dev20240328.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,239 @@
|
|
1
|
+
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
|
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 contextlib
|
18
|
+
import hashlib
|
19
|
+
import inspect
|
20
|
+
from abc import abstractmethod
|
21
|
+
from copy import deepcopy
|
22
|
+
from datetime import timedelta
|
23
|
+
from functools import wraps
|
24
|
+
from typing import Any, Callable, Protocol, TypeVar, overload
|
25
|
+
|
26
|
+
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
27
|
+
from streamlit.runtime.metrics_util import gather_metrics
|
28
|
+
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
29
|
+
from streamlit.time_util import time_to_seconds
|
30
|
+
|
31
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
32
|
+
Fragment = Callable[[], Any]
|
33
|
+
|
34
|
+
|
35
|
+
class FragmentStorage(Protocol):
|
36
|
+
"""A key-value store for Fragments. Used to implement the @st.experimental_fragment
|
37
|
+
decorator.
|
38
|
+
|
39
|
+
We intentionally define this as its own protocol despite how generic it appears to
|
40
|
+
be at first glance. The reason why is that, in any case where fragments aren't just
|
41
|
+
stored as Python closures in memory, storing and retrieving Fragments will generally
|
42
|
+
involve serializing and deserializing function bytecode, which is a tricky aspect
|
43
|
+
to implementing FragmentStorages that won't generally appear with our other *Storage
|
44
|
+
protocols.
|
45
|
+
"""
|
46
|
+
|
47
|
+
@abstractmethod
|
48
|
+
def get(self, key: str) -> Fragment:
|
49
|
+
"""Returns the stored fragment for the given key."""
|
50
|
+
raise NotImplementedError
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def set(self, key: str, value: Fragment) -> None:
|
54
|
+
"""Saves a fragment under the given key."""
|
55
|
+
raise NotImplementedError
|
56
|
+
|
57
|
+
@abstractmethod
|
58
|
+
def delete(self, key: str) -> None:
|
59
|
+
"""Delete the fragment corresponding to the given key."""
|
60
|
+
raise NotImplementedError
|
61
|
+
|
62
|
+
@abstractmethod
|
63
|
+
def clear(self) -> None:
|
64
|
+
"""Remove all fragments saved in this FragmentStorage."""
|
65
|
+
raise NotImplementedError
|
66
|
+
|
67
|
+
|
68
|
+
# NOTE: Ideally, we'd like to add a MemoryFragmentStorageStatProvider implementation to
|
69
|
+
# keep track of memory usage due to fragments, but doing something like this ends up
|
70
|
+
# being difficult in practice as the memory usage of a closure is hard to measure (the
|
71
|
+
# vendored implementation of pympler.asizeof that we use elsewhere is unable to measure
|
72
|
+
# the size of a function).
|
73
|
+
class MemoryFragmentStorage(FragmentStorage):
|
74
|
+
"""A simple, memory-backed implementation of FragmentStorage.
|
75
|
+
|
76
|
+
MemoryFragmentStorage is just a wrapper around a plain Python dict that complies with
|
77
|
+
the FragmentStorage protocol.
|
78
|
+
"""
|
79
|
+
|
80
|
+
def __init__(self):
|
81
|
+
self._fragments: dict[str, Fragment] = {}
|
82
|
+
|
83
|
+
def get(self, key: str) -> Fragment:
|
84
|
+
return self._fragments[key]
|
85
|
+
|
86
|
+
def set(self, key: str, value: Fragment) -> None:
|
87
|
+
self._fragments[key] = value
|
88
|
+
|
89
|
+
def delete(self, key: str) -> None:
|
90
|
+
del self._fragments[key]
|
91
|
+
|
92
|
+
def clear(self) -> None:
|
93
|
+
self._fragments.clear()
|
94
|
+
|
95
|
+
|
96
|
+
@overload
|
97
|
+
def fragment(
|
98
|
+
func: F,
|
99
|
+
*,
|
100
|
+
run_every: int | float | timedelta | str | None = None,
|
101
|
+
) -> F:
|
102
|
+
...
|
103
|
+
|
104
|
+
|
105
|
+
# Support being able to pass parameters to this decorator (that is, being able to write
|
106
|
+
# `@fragment(run_every=5.0)`).
|
107
|
+
@overload
|
108
|
+
def fragment(
|
109
|
+
func: None = None,
|
110
|
+
*,
|
111
|
+
run_every: int | float | timedelta | str | None = None,
|
112
|
+
) -> Callable[[F], F]:
|
113
|
+
...
|
114
|
+
|
115
|
+
|
116
|
+
@gather_metrics("experimental_fragment")
|
117
|
+
def fragment(
|
118
|
+
func: F | None = None,
|
119
|
+
*,
|
120
|
+
run_every: int | float | timedelta | str | None = None,
|
121
|
+
) -> Callable[[F], F] | F:
|
122
|
+
"""Allow a function to be run independently of the full script.
|
123
|
+
|
124
|
+
Functions decorated with ``@st.experimental_fragment`` are handled specially within
|
125
|
+
an app: when a widget created within an invocation of the function (a fragment) is
|
126
|
+
interacted with, then only that fragment is rerun rather than the full streamlit app.
|
127
|
+
|
128
|
+
Parameters
|
129
|
+
----------
|
130
|
+
run_every: int, float, timedelta, str, or None
|
131
|
+
If set, fragments created from this function rerun periodically at the specified
|
132
|
+
time interval.
|
133
|
+
|
134
|
+
Example
|
135
|
+
-------
|
136
|
+
The following example demonstrates basic usage of ``@st.experimental_fragment``. In
|
137
|
+
this app, clicking on the "rerun full script" button will increment both counters,
|
138
|
+
but the "rerun fragment" button will only increment the counter within the fragment.
|
139
|
+
|
140
|
+
```python3
|
141
|
+
import streamlit as st
|
142
|
+
|
143
|
+
if "script_runs" not in st.session_state:
|
144
|
+
st.session_state.script_runs = 0
|
145
|
+
st.session_state.fragment_runs = 0
|
146
|
+
|
147
|
+
@st.experimental_fragment
|
148
|
+
def fragment():
|
149
|
+
st.button("rerun fragment")
|
150
|
+
st.write(f"fragment runs: {st.session_state.fragment_runs}")
|
151
|
+
st.session_state.fragment_runs += 1
|
152
|
+
|
153
|
+
fragment()
|
154
|
+
|
155
|
+
st.button("rerun full script")
|
156
|
+
st.write(f"full script runs: {st.session_state.script_runs}")
|
157
|
+
st.session_state.script_runs += 1
|
158
|
+
```
|
159
|
+
"""
|
160
|
+
|
161
|
+
if func is None:
|
162
|
+
# Support passing the params via function decorator
|
163
|
+
def wrapper(f: F) -> F:
|
164
|
+
return fragment(
|
165
|
+
func=f,
|
166
|
+
run_every=run_every,
|
167
|
+
)
|
168
|
+
|
169
|
+
return wrapper
|
170
|
+
else:
|
171
|
+
non_optional_func = func
|
172
|
+
|
173
|
+
@wraps(non_optional_func)
|
174
|
+
def wrap(*args, **kwargs):
|
175
|
+
from streamlit.delta_generator import dg_stack
|
176
|
+
|
177
|
+
ctx = get_script_run_ctx()
|
178
|
+
if ctx is None:
|
179
|
+
return
|
180
|
+
|
181
|
+
cursors_snapshot = deepcopy(ctx.cursors)
|
182
|
+
dg_stack_snapshot = deepcopy(dg_stack.get())
|
183
|
+
active_dg = dg_stack_snapshot[-1]
|
184
|
+
h = hashlib.new("md5")
|
185
|
+
h.update(
|
186
|
+
f"{non_optional_func.__module__}.{non_optional_func.__qualname__}{active_dg._get_delta_path_str()}".encode(
|
187
|
+
"utf-8"
|
188
|
+
)
|
189
|
+
)
|
190
|
+
fragment_id = h.hexdigest()
|
191
|
+
|
192
|
+
def wrapped_fragment():
|
193
|
+
import streamlit as st
|
194
|
+
|
195
|
+
# NOTE: We need to call get_script_run_ctx here again and can't just use the
|
196
|
+
# value of ctx from above captured by the closure because subsequent
|
197
|
+
# fragment runs will generally run in a new script run, thus we'll have a
|
198
|
+
# new ctx.
|
199
|
+
ctx = get_script_run_ctx(suppress_warning=True)
|
200
|
+
assert ctx is not None
|
201
|
+
|
202
|
+
if ctx.fragment_ids_this_run:
|
203
|
+
# This script run is a run of one or more fragments. We restore the
|
204
|
+
# state of ctx.cursors and dg_stack to the snapshots we took when this
|
205
|
+
# fragment was declared.
|
206
|
+
ctx.cursors = deepcopy(cursors_snapshot)
|
207
|
+
dg_stack.set(deepcopy(dg_stack_snapshot))
|
208
|
+
else:
|
209
|
+
# Otherwise, we must be in a full script run. We need to temporarily set
|
210
|
+
# ctx.current_fragment_id so that elements corresponding to this
|
211
|
+
# fragment get tagged with the appropriate ID. ctx.current_fragment_id
|
212
|
+
# gets reset after the fragment function finishes running.
|
213
|
+
ctx.current_fragment_id = fragment_id
|
214
|
+
|
215
|
+
try:
|
216
|
+
with st.container():
|
217
|
+
result = non_optional_func(*args, **kwargs)
|
218
|
+
finally:
|
219
|
+
ctx.current_fragment_id = None
|
220
|
+
|
221
|
+
return result
|
222
|
+
|
223
|
+
ctx.fragment_storage.set(fragment_id, wrapped_fragment)
|
224
|
+
|
225
|
+
if run_every:
|
226
|
+
msg = ForwardMsg()
|
227
|
+
msg.auto_rerun.interval = time_to_seconds(run_every)
|
228
|
+
msg.auto_rerun.fragment_id = fragment_id
|
229
|
+
ctx.enqueue(msg)
|
230
|
+
|
231
|
+
return wrapped_fragment()
|
232
|
+
|
233
|
+
with contextlib.suppress(AttributeError):
|
234
|
+
# Make this a well-behaved decorator by preserving important function
|
235
|
+
# attributes.
|
236
|
+
wrap.__dict__.update(non_optional_func.__dict__)
|
237
|
+
wrap.__signature__ = inspect.signature(non_optional_func) # type: ignore
|
238
|
+
|
239
|
+
return wrap
|
@@ -427,12 +427,17 @@ def create_page_profile_message(
|
|
427
427
|
uncaught_exception: str | None = None,
|
428
428
|
) -> ForwardMsg:
|
429
429
|
"""Create and return the full PageProfile ForwardMsg."""
|
430
|
+
# Local import to prevent circular dependencies
|
431
|
+
from streamlit.runtime.scriptrunner import get_script_run_ctx
|
432
|
+
|
430
433
|
msg = ForwardMsg()
|
431
|
-
msg.page_profile
|
432
|
-
|
433
|
-
|
434
|
+
page_profile = msg.page_profile
|
435
|
+
|
436
|
+
page_profile.commands.extend(commands)
|
437
|
+
page_profile.exec_time = exec_time
|
438
|
+
page_profile.prep_time = prep_time
|
434
439
|
|
435
|
-
|
440
|
+
page_profile.headless = config.get_option("server.headless")
|
436
441
|
|
437
442
|
# Collect all config options that have been manually set
|
438
443
|
config_options: set[str] = set()
|
@@ -447,7 +452,7 @@ def create_page_profile_message(
|
|
447
452
|
option_name = f"{option_name}:default"
|
448
453
|
config_options.add(option_name)
|
449
454
|
|
450
|
-
|
455
|
+
page_profile.config.extend(config_options)
|
451
456
|
|
452
457
|
# Check the predefined set of modules for attribution
|
453
458
|
attributions: set[str] = {
|
@@ -456,11 +461,14 @@ def create_page_profile_message(
|
|
456
461
|
if attribution in sys.modules
|
457
462
|
}
|
458
463
|
|
459
|
-
|
460
|
-
|
461
|
-
|
464
|
+
page_profile.os = str(sys.platform)
|
465
|
+
page_profile.timezone = str(time.tzname)
|
466
|
+
page_profile.attributions.extend(attributions)
|
462
467
|
|
463
468
|
if uncaught_exception:
|
464
|
-
|
469
|
+
page_profile.uncaught_exception = uncaught_exception
|
470
|
+
|
471
|
+
if ctx := get_script_run_ctx():
|
472
|
+
page_profile.is_fragment_run = bool(ctx.current_fragment_id)
|
465
473
|
|
466
474
|
return msg
|
streamlit/runtime/runtime.py
CHANGED
@@ -22,8 +22,6 @@ from enum import Enum
|
|
22
22
|
from typing import TYPE_CHECKING, Awaitable, Final, NamedTuple
|
23
23
|
|
24
24
|
from streamlit import config
|
25
|
-
from streamlit.components.lib.local_component_registry import LocalComponentRegistry
|
26
|
-
from streamlit.components.types.base_component_registry import BaseComponentRegistry
|
27
25
|
from streamlit.logger import get_logger
|
28
26
|
from streamlit.proto.BackMsg_pb2 import BackMsg
|
29
27
|
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
@@ -98,11 +96,6 @@ class RuntimeConfig:
|
|
98
96
|
default_factory=LocalDiskCacheStorageManager
|
99
97
|
)
|
100
98
|
|
101
|
-
# The ComponentRegistry instance to use.
|
102
|
-
component_registry: BaseComponentRegistry = field(
|
103
|
-
default_factory=LocalComponentRegistry
|
104
|
-
)
|
105
|
-
|
106
99
|
# The SessionManager class to be used.
|
107
100
|
session_manager_class: type[SessionManager] = WebsocketSessionManager
|
108
101
|
|
@@ -112,6 +105,12 @@ class RuntimeConfig:
|
|
112
105
|
# True if the command used to start Streamlit was `streamlit hello`.
|
113
106
|
is_hello: bool = False
|
114
107
|
|
108
|
+
# TODO(vdonato): Eventually add a new fragment_storage_class field enabling the code
|
109
|
+
# creating a new Streamlit Runtime to configure the FragmentStorage instances
|
110
|
+
# created by each new AppSession. We choose not to do this for now to avoid adding
|
111
|
+
# additional complexity to RuntimeConfig/SessionManager/etc when it's unlikely
|
112
|
+
# we'll have a custom implementation of this class anytime soon.
|
113
|
+
|
115
114
|
|
116
115
|
class RuntimeState(Enum):
|
117
116
|
INITIAL = "INITIAL"
|
@@ -196,7 +195,6 @@ class Runtime:
|
|
196
195
|
self._state = RuntimeState.INITIAL
|
197
196
|
|
198
197
|
# Initialize managers
|
199
|
-
self._component_registry = config.component_registry
|
200
198
|
self._message_cache = ForwardMsgCache()
|
201
199
|
self._uploaded_file_mgr = config.uploaded_file_manager
|
202
200
|
self._media_file_mgr = MediaFileManager(storage=config.media_file_storage)
|
@@ -222,10 +220,6 @@ class Runtime:
|
|
222
220
|
def state(self) -> RuntimeState:
|
223
221
|
return self._state
|
224
222
|
|
225
|
-
@property
|
226
|
-
def component_registry(self) -> BaseComponentRegistry:
|
227
|
-
return self._component_registry
|
228
|
-
|
229
223
|
@property
|
230
224
|
def message_cache(self) -> ForwardMsgCache:
|
231
225
|
return self._message_cache
|
@@ -16,10 +16,12 @@
|
|
16
16
|
|
17
17
|
from __future__ import annotations
|
18
18
|
|
19
|
-
|
19
|
+
import math
|
20
|
+
from datetime import timedelta
|
21
|
+
from typing import Any, Literal, overload
|
20
22
|
|
21
23
|
from streamlit import config
|
22
|
-
from streamlit.errors import MarkdownFormattedException
|
24
|
+
from streamlit.errors import MarkdownFormattedException, StreamlitAPIException
|
23
25
|
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
|
24
26
|
from streamlit.runtime.forward_msg_cache import populate_hash_if_needed
|
25
27
|
|
@@ -54,6 +56,17 @@ of the client's browser and the Streamlit server._
|
|
54
56
|
)
|
55
57
|
|
56
58
|
|
59
|
+
class BadDurationStringError(StreamlitAPIException):
|
60
|
+
"""Raised when a bad duration argument string is passed."""
|
61
|
+
|
62
|
+
def __init__(self, duration: str):
|
63
|
+
MarkdownFormattedException.__init__(
|
64
|
+
self,
|
65
|
+
"TTL string doesn't look right. It should be formatted as"
|
66
|
+
f"`'1d2h34m'` or `2 days`, for example. Got: {duration}",
|
67
|
+
)
|
68
|
+
|
69
|
+
|
57
70
|
def is_cacheable_msg(msg: ForwardMsg) -> bool:
|
58
71
|
"""True if the given message qualifies for caching."""
|
59
72
|
if msg.WhichOneof("type") in {"ref_hash", "initialize"}:
|
@@ -62,6 +75,45 @@ def is_cacheable_msg(msg: ForwardMsg) -> bool:
|
|
62
75
|
return msg.ByteSize() >= int(config.get_option("global.minCachedMessageSize"))
|
63
76
|
|
64
77
|
|
78
|
+
@overload
|
79
|
+
def duration_to_seconds(
|
80
|
+
ttl: float | timedelta | str | None, *, coerce_none_to_inf: Literal[False]
|
81
|
+
) -> float | None:
|
82
|
+
...
|
83
|
+
|
84
|
+
|
85
|
+
@overload
|
86
|
+
def duration_to_seconds(ttl: float | timedelta | str | None) -> float:
|
87
|
+
...
|
88
|
+
|
89
|
+
|
90
|
+
def duration_to_seconds(
|
91
|
+
ttl: float | timedelta | str | None, *, coerce_none_to_inf: bool = True
|
92
|
+
) -> float | None:
|
93
|
+
"""
|
94
|
+
Convert a ttl value to a float representing "number of seconds".
|
95
|
+
"""
|
96
|
+
if coerce_none_to_inf and ttl is None:
|
97
|
+
return math.inf
|
98
|
+
if isinstance(ttl, timedelta):
|
99
|
+
return ttl.total_seconds()
|
100
|
+
if isinstance(ttl, str):
|
101
|
+
import numpy as np
|
102
|
+
import pandas as pd
|
103
|
+
|
104
|
+
try:
|
105
|
+
out: float = pd.Timedelta(ttl).total_seconds()
|
106
|
+
except ValueError as ex:
|
107
|
+
raise BadDurationStringError(ttl) from ex
|
108
|
+
|
109
|
+
if np.isnan(out):
|
110
|
+
raise BadDurationStringError(ttl)
|
111
|
+
|
112
|
+
return out
|
113
|
+
|
114
|
+
return ttl
|
115
|
+
|
116
|
+
|
65
117
|
def serialize_forward_msg(msg: ForwardMsg) -> bytes:
|
66
118
|
"""Serialize a ForwardMsg to send to a client.
|
67
119
|
|
@@ -15,7 +15,7 @@
|
|
15
15
|
from __future__ import annotations
|
16
16
|
|
17
17
|
import threading
|
18
|
-
from dataclasses import dataclass
|
18
|
+
from dataclasses import dataclass, field
|
19
19
|
from enum import Enum
|
20
20
|
from typing import cast
|
21
21
|
|
@@ -46,6 +46,7 @@ class RerunData:
|
|
46
46
|
widget_states: WidgetStates | None = None
|
47
47
|
page_script_hash: str = ""
|
48
48
|
page_name: str = ""
|
49
|
+
fragment_id_queue: list[str] = field(default_factory=list)
|
49
50
|
|
50
51
|
def __repr__(self) -> str:
|
51
52
|
return util.repr_(self)
|
@@ -103,41 +104,46 @@ class ScriptRequests:
|
|
103
104
|
return False
|
104
105
|
|
105
106
|
if self._state == ScriptRequestType.CONTINUE:
|
106
|
-
#
|
107
|
-
# unconditionally
|
107
|
+
# The script is currently running, and we haven't received a request to
|
108
|
+
# rerun it as of yet. We can handle a rerun request unconditionally so
|
109
|
+
# just change self._state and set self._rerun_data.
|
108
110
|
self._state = ScriptRequestType.RERUN
|
109
111
|
self._rerun_data = new_data
|
110
112
|
return True
|
111
113
|
|
112
114
|
if self._state == ScriptRequestType.RERUN:
|
113
|
-
#
|
114
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
)
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
115
|
+
# We already have an existing Rerun request, so we can coalesce the new
|
116
|
+
# rerun request into the existing one.
|
117
|
+
|
118
|
+
coalesced_states = coalesce_widget_states(
|
119
|
+
self._rerun_data.widget_states, new_data.widget_states
|
120
|
+
)
|
121
|
+
|
122
|
+
if new_data.fragment_id_queue:
|
123
|
+
# This RERUN request corresponds to a fragment run. We append the
|
124
|
+
# new fragment ID to the end of the current fragment_id_queue if it
|
125
|
+
# isn't already contained in it.
|
126
|
+
fragment_id_queue = [*self._rerun_data.fragment_id_queue]
|
127
|
+
if (
|
128
|
+
# new_data.fragment_id_queue is always a singleton
|
129
|
+
(new_fragment_id := new_data.fragment_id_queue[0])
|
130
|
+
not in fragment_id_queue
|
131
|
+
):
|
132
|
+
fragment_id_queue.append(new_fragment_id)
|
133
|
+
else:
|
134
|
+
# Otherwise, this is a request to rerun the full script, so we want
|
135
|
+
# to clear out any fragments we have queued to run since they'll all
|
136
|
+
# be run with the full script anyway.
|
137
|
+
fragment_id_queue = []
|
138
|
+
|
139
|
+
self._rerun_data = RerunData(
|
140
|
+
query_string=new_data.query_string,
|
141
|
+
widget_states=coalesced_states,
|
142
|
+
page_script_hash=new_data.page_script_hash,
|
143
|
+
page_name=new_data.page_name,
|
144
|
+
fragment_id_queue=fragment_id_queue,
|
145
|
+
)
|
146
|
+
|
141
147
|
return True
|
142
148
|
|
143
149
|
# We'll never get here
|
@@ -146,22 +152,32 @@ class ScriptRequests:
|
|
146
152
|
def on_scriptrunner_yield(self) -> ScriptRequest | None:
|
147
153
|
"""Called by the ScriptRunner when it's at a yield point.
|
148
154
|
|
149
|
-
If we have no request
|
155
|
+
If we have no request or a RERUN request corresponding to one or more fragments,
|
156
|
+
return None.
|
150
157
|
|
151
|
-
If we have a RERUN request, return the request and set our internal
|
158
|
+
If we have a (full script) RERUN request, return the request and set our internal
|
152
159
|
state to CONTINUE.
|
153
160
|
|
154
161
|
If we have a STOP request, return the request and remain stopped.
|
155
162
|
"""
|
156
|
-
if self._state == ScriptRequestType.CONTINUE
|
157
|
-
#
|
158
|
-
#
|
159
|
-
|
163
|
+
if self._state == ScriptRequestType.CONTINUE or (
|
164
|
+
# Reruns corresponding to fragments should *not* cancel the current script
|
165
|
+
# run as doing so will affect elements outside of the fragment.
|
166
|
+
self._state == ScriptRequestType.RERUN
|
167
|
+
and self._rerun_data.fragment_id_queue
|
168
|
+
):
|
169
|
+
# We avoid taking the lock in the common cases of having no request and
|
170
|
+
# having a RERUN request corresponding to >=1 fragments. If a STOP or
|
171
|
+
# (full script) RERUN request is received between the `if` and `return`, it
|
172
|
+
# will be handled at the next `on_scriptrunner_yield`, or when
|
160
173
|
# `on_scriptrunner_ready` is called.
|
161
174
|
return None
|
162
175
|
|
163
176
|
with self._lock:
|
164
177
|
if self._state == ScriptRequestType.RERUN:
|
178
|
+
if self._rerun_data.fragment_id_queue:
|
179
|
+
return None
|
180
|
+
|
165
181
|
self._state = ScriptRequestType.CONTINUE
|
166
182
|
return ScriptRequest(ScriptRequestType.RERUN, self._rerun_data)
|
167
183
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
17
17
|
import collections
|
18
18
|
import threading
|
19
19
|
from dataclasses import dataclass, field
|
20
|
-
from typing import Callable, Counter, Dict, Final, Union
|
20
|
+
from typing import TYPE_CHECKING, Callable, Counter, Dict, Final, Union
|
21
21
|
from urllib import parse
|
22
22
|
|
23
23
|
from typing_extensions import TypeAlias
|
@@ -31,6 +31,9 @@ from streamlit.runtime.scriptrunner.script_requests import ScriptRequests
|
|
31
31
|
from streamlit.runtime.state import SafeSessionState
|
32
32
|
from streamlit.runtime.uploaded_file_manager import UploadedFileManager
|
33
33
|
|
34
|
+
if TYPE_CHECKING:
|
35
|
+
from streamlit.runtime.fragment import FragmentStorage
|
36
|
+
|
34
37
|
_LOGGER: Final = get_logger(__name__)
|
35
38
|
|
36
39
|
UserInfo: TypeAlias = Dict[str, Union[str, None]]
|
@@ -59,6 +62,7 @@ class ScriptRunContext:
|
|
59
62
|
main_script_path: str
|
60
63
|
page_script_hash: str
|
61
64
|
user_info: UserInfo
|
65
|
+
fragment_storage: "FragmentStorage"
|
62
66
|
|
63
67
|
gather_usage_stats: bool = False
|
64
68
|
command_tracking_deactivated: bool = False
|
@@ -71,12 +75,19 @@ class ScriptRunContext:
|
|
71
75
|
form_ids_this_run: set[str] = field(default_factory=set)
|
72
76
|
cursors: dict[int, "streamlit.cursor.RunningCursor"] = field(default_factory=dict)
|
73
77
|
script_requests: ScriptRequests | None = None
|
78
|
+
current_fragment_id: str | None = None
|
79
|
+
fragment_ids_this_run: set[str] | None = None
|
74
80
|
|
75
81
|
# TODO(willhuang1997): Remove this variable when experimental query params are removed
|
76
82
|
_experimental_query_params_used = False
|
77
83
|
_production_query_params_used = False
|
78
84
|
|
79
|
-
def reset(
|
85
|
+
def reset(
|
86
|
+
self,
|
87
|
+
query_string: str = "",
|
88
|
+
page_script_hash: str = "",
|
89
|
+
fragment_ids_this_run: set[str] | None = None,
|
90
|
+
) -> None:
|
80
91
|
self.cursors = {}
|
81
92
|
self.widget_ids_this_run = set()
|
82
93
|
self.widget_user_keys_this_run = set()
|
@@ -89,6 +100,8 @@ class ScriptRunContext:
|
|
89
100
|
self.command_tracking_deactivated: bool = False
|
90
101
|
self.tracked_commands = []
|
91
102
|
self.tracked_commands_counter = collections.Counter()
|
103
|
+
self.current_fragment_id = None
|
104
|
+
self.fragment_ids_this_run = fragment_ids_this_run
|
92
105
|
|
93
106
|
parsed_query_params = parse.parse_qs(query_string, keep_blank_values=True)
|
94
107
|
with self.session_state.query_params() as qp:
|