streamlit-nightly 1.52.3.dev20260104__py3-none-any.whl → 1.52.3.dev20260105__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/runtime/app_session.py +15 -2
- streamlit/runtime/caching/__init__.py +8 -0
- streamlit/runtime/caching/cache_data_api.py +76 -8
- streamlit/runtime/caching/cache_resource_api.py +79 -6
- streamlit/runtime/caching/cache_utils.py +43 -2
- streamlit/runtime/stats.py +1 -0
- streamlit/runtime/websocket_session_manager.py +37 -1
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +300 -300
- streamlit/static/static/js/{ErrorOutline.esm.DB4TeKXQ.js → ErrorOutline.esm.CUbPpREP.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.B1rsRcZM.js → FileDownload.esm.DetkHTcN.js} +1 -1
- streamlit/static/static/js/{FileHelper.Dy-ENeyM.js → FileHelper.BwIyMxu6.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.wCJbsz1D.js → FormClearHelper.CASwqls6.js} +1 -1
- streamlit/static/static/js/{InputInstructions.h-b7uu-m.js → InputInstructions.S3adIyNg.js} +1 -1
- streamlit/static/static/js/{Particles.DMdiwhvk.js → Particles.RThwaGLL.js} +1 -1
- streamlit/static/static/js/{ProgressBar.InacggUW.js → ProgressBar.C5R-2EpR.js} +1 -1
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.CyryJTW-.js → StreamlitSyntaxHighlighter.Y7KVbKtx.js} +1 -1
- streamlit/static/static/js/{TableChart.esm.DCIQ3yuB.js → TableChart.esm.Yg3weFJJ.js} +1 -1
- streamlit/static/static/js/{Toolbar.CdfNxtO4.js → Toolbar.BLFSj5n_.js} +1 -1
- streamlit/static/static/js/{WidgetLabelHelpIconInline.DLM1gtZz.js → WidgetLabelHelpIconInline.DAWJEVd6.js} +1 -1
- streamlit/static/static/js/{base-input.BCIrgNRb.js → base-input.CHeZetve.js} +1 -1
- streamlit/static/static/js/{checkbox.cSjLtYx0.js → checkbox.BK8sNhcL.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.BD_DYT7M.js → createDownloadLinkElement._lShGDOq.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.BWp1Ajhl.js → data-grid-overlay-editor.ssbLvS2J.js} +1 -1
- streamlit/static/static/js/{downloader.CEDLkUji.js → downloader.CZdoNBU_.js} +1 -1
- streamlit/static/static/js/embed.2TWJ6qNB.js +195 -0
- streamlit/static/static/js/{es6.C3cvBhbs.js → es6.D41As5l9.js} +2 -2
- streamlit/static/static/js/{formatNumber.Bsp-qOPC.js → formatNumber.QuIArRkA.js} +1 -1
- streamlit/static/static/js/{iconPosition.DIRj5SlL.js → iconPosition.CSpYbZdy.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.tjj-iaW8.js → iframeResizer.contentWindow.Ba3c7d3E.js} +1 -1
- streamlit/static/static/js/{index.Y1-tp-v8.js → index.9i_-lwmj.js} +1 -1
- streamlit/static/static/js/{index.XOden_TM.js → index.B4ZFNrs2.js} +6 -6
- streamlit/static/static/js/{index.BdwE460H.js → index.B6YM-IfX.js} +1 -1
- streamlit/static/static/js/{index.BrhVkvAP.js → index.B8XnIwXu.js} +1 -1
- streamlit/static/static/js/{index.BXggolS2.js → index.BAHhsNEO.js} +1 -1
- streamlit/static/static/js/{index.CU7z3OC0.js → index.BCDbmK91.js} +1 -1
- streamlit/static/static/js/{index.DTtGJWwh.js → index.BDu_MJqs.js} +1 -1
- streamlit/static/static/js/{index.oX51i0fi.js → index.BI_fnYvO.js} +1 -1
- streamlit/static/static/js/{index.lGfrZbsE.js → index.BOIgWFBo.js} +1 -1
- streamlit/static/static/js/{index.CLchkPc0.js → index.BOvjmPsN.js} +1 -1
- streamlit/static/static/js/{index.DPXljG88.js → index.BYH1e4xe.js} +1 -1
- streamlit/static/static/js/{index.CKv21ig2.js → index.BnkFb6TA.js} +1 -1
- streamlit/static/static/js/{index.CMToM8tv.js → index.BoY3Kkcd.js} +1 -1
- streamlit/static/static/js/{index.xp9Iy5jw.js → index.BvVfzDZQ.js} +1 -1
- streamlit/static/static/js/{index.hkwmBmj1.js → index.Bw-KHlow.js} +1 -1
- streamlit/static/static/js/{index.C6KU1c_I.js → index.Bz3MkLgx.js} +1 -1
- streamlit/static/static/js/{index.Y9iivd6m.js → index.C761egln.js} +1 -1
- streamlit/static/static/js/{index.ClZw_QCU.js → index.C7V9OjPg.js} +1 -1
- streamlit/static/static/js/{index.VAsk7L3S.js → index.C8z5LOYQ.js} +1 -1
- streamlit/static/static/js/{index.Y-fts05K.js → index.CYiKO5um.js} +1 -1
- streamlit/static/static/js/{index.CP-UEHmv.js → index.Chj6qsmB.js} +1 -1
- streamlit/static/static/js/{index.CkWtzTE7.js → index.Clrm_eTk.js} +1 -1
- streamlit/static/static/js/{index.Cma1PKQE.js → index.CuZMPicb.js} +1 -1
- streamlit/static/static/js/{index.BgFWPu68.js → index.D2GAddP5.js} +1 -1
- streamlit/static/static/js/{index.CkA0etie.js → index.D6rDKuAr.js} +1 -1
- streamlit/static/static/js/{index.CJNYj-GU.js → index.DBEtWjU1.js} +1 -1
- streamlit/static/static/js/{index.D9QOVKpD.js → index.DDf8sbO4.js} +1 -1
- streamlit/static/static/js/{index.REjZL5tN.js → index.DDmNxuM1.js} +1 -1
- streamlit/static/static/js/index.DFCPECXZ.js +1 -0
- streamlit/static/static/js/{index.Cqo3gLMs.js → index.DLplCBmm.js} +1 -1
- streamlit/static/static/js/{index.DERvWU-H.js → index.DRcHLhCK.js} +1 -1
- streamlit/static/static/js/{index.BIWD38us.js → index.DSMOcJzt.js} +1 -1
- streamlit/static/static/js/{index.CQk1zChj.js → index.DWr4fsaW.js} +1 -1
- streamlit/static/static/js/{index.BzF2fUQY.js → index.DZSrtq6x.js} +1 -1
- streamlit/static/static/js/{index.BU_tKVxv.js → index.DceyCzDe.js} +1 -1
- streamlit/static/static/js/{index.ZVTCDY95.js → index.Dd0uREcX.js} +1 -1
- streamlit/static/static/js/{index.ChoFIv3e.js → index.DnWt2J6g.js} +1 -1
- streamlit/static/static/js/{index.qsDXxiva.js → index.DrsyU_tH.js} +1 -1
- streamlit/static/static/js/{index.DbV0rUEu.js → index.KQLzM7MF.js} +1 -1
- streamlit/static/static/js/{index.glmKpzAC.js → index.OlwiZDpn.js} +1 -1
- streamlit/static/static/js/{index.B0DCNp1Y.js → index.Q_E7sonx.js} +6 -6
- streamlit/static/static/js/{index.DjxVfGrI.js → index.WQewRvlT.js} +1 -1
- streamlit/static/static/js/{index.CpgN7t8w.js → index.WRCuRhqg.js} +1 -1
- streamlit/static/static/js/{index.7mJnVlDm.js → index.g1YybQQ9.js} +1 -1
- streamlit/static/static/js/{index.hkR1h-xb.js → index.nbcFTn_s.js} +1 -1
- streamlit/static/static/js/{index.D-LmUIYK.js → index.thfM5Jp-.js} +1 -1
- streamlit/static/static/js/{index.2-6ocypr.js → index.uYBEeDUr.js} +1 -1
- streamlit/static/static/js/{index.65xiOHvb.js → index.zcrZ8Dj9.js} +1 -1
- streamlit/static/static/js/{input.BLVfcIAQ.js → input.BfQ-NLYJ.js} +1 -1
- streamlit/static/static/js/{main.DOT_xSoS.js → main.kRp_qJ7D.js} +1 -1
- streamlit/static/static/js/{memory.BU-JM8pI.js → memory.BkmZS8L_.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.BI4bxl54.js → number-overlay-editor.DG6TifvP.js} +1 -1
- streamlit/static/static/js/{pandasStylerUtils.BAO8bi8z.js → pandasStylerUtils.2q7Waiua.js} +1 -1
- streamlit/static/static/js/{sandbox.BEvKSsDs.js → sandbox.CNlPmm2Y.js} +1 -1
- streamlit/static/static/js/{styled-components.BeC32T5O.js → styled-components.BzgaiWCN.js} +1 -1
- streamlit/static/static/js/{throttle.BZ3PXlI9.js → throttle.C6Ziz6X5.js} +1 -1
- streamlit/static/static/js/{timepicker.BThGAJl8.js → timepicker.BaHSf28n.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.CFvVrWwv.js → toConsumableArray.BNGCByOf.js} +1 -1
- streamlit/static/static/js/uniqueId.Nvl4r5vW.js +1 -0
- streamlit/static/static/js/{useBasicWidgetState.DRzRDWGT.js → useBasicWidgetState.BcW4Dq9k.js} +1 -1
- streamlit/static/static/js/{useIntlLocale.CdE4T1Ur.js → useIntlLocale.BmLgec-0.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.BVefdTg1.js → useTextInputAutoExpand.BR8fAFbd.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.Ce3tYwUF.js → useUpdateUiValue.Dg1Zd8gd.js} +1 -1
- streamlit/static/static/js/{useWaveformController.CKTio31b.js → useWaveformController.Ch2JLD06.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.BcZACcrB.js → withCalculatedWidth.YVnNIVT2.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.CZmpHubt.js → withFullScreenWrapper.DjieznUq.js} +1 -1
- {streamlit_nightly-1.52.3.dev20260104.dist-info → streamlit_nightly-1.52.3.dev20260105.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.52.3.dev20260104.dist-info → streamlit_nightly-1.52.3.dev20260105.dist-info}/RECORD +102 -102
- streamlit/static/static/js/embed.Dn7p4MYf.js +0 -195
- streamlit/static/static/js/index.D_MQwywr.js +0 -1
- streamlit/static/static/js/uniqueId.DDvAwLSm.js +0 -1
- {streamlit_nightly-1.52.3.dev20260104.data → streamlit_nightly-1.52.3.dev20260105.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.52.3.dev20260104.dist-info → streamlit_nightly-1.52.3.dev20260105.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.52.3.dev20260104.dist-info → streamlit_nightly-1.52.3.dev20260105.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.52.3.dev20260104.dist-info → streamlit_nightly-1.52.3.dev20260105.dist-info}/top_level.txt +0 -0
streamlit/runtime/app_session.py
CHANGED
|
@@ -221,6 +221,15 @@ class AppSession:
|
|
|
221
221
|
self._stop_config_listener = None
|
|
222
222
|
self._stop_pages_listener = None
|
|
223
223
|
|
|
224
|
+
def clear_session_caches(self) -> None:
|
|
225
|
+
"""Clears session-level caches for this session.
|
|
226
|
+
|
|
227
|
+
This should be called when a session is disconnected or shut down, since this
|
|
228
|
+
ensures memory is freed up and resource release hooks are called.
|
|
229
|
+
"""
|
|
230
|
+
caching.clear_session_data_cache(self.id)
|
|
231
|
+
caching.clear_session_resource_cache(self.id)
|
|
232
|
+
|
|
224
233
|
def flush_browser_queue(self) -> list[ForwardMsg]:
|
|
225
234
|
"""Clear the forward message queue and return the messages it contained.
|
|
226
235
|
|
|
@@ -264,6 +273,9 @@ class AppSession:
|
|
|
264
273
|
# generally already done so by the time we get here.
|
|
265
274
|
self.disconnect_file_watchers()
|
|
266
275
|
|
|
276
|
+
# Clear any session caches. This ensures shutdown hooks are called.
|
|
277
|
+
self.clear_session_caches()
|
|
278
|
+
|
|
267
279
|
def _enqueue_forward_msg(self, msg: ForwardMsg) -> None:
|
|
268
280
|
"""Enqueue a new ForwardMsg to our browser queue.
|
|
269
281
|
|
|
@@ -691,9 +703,10 @@ class AppSession:
|
|
|
691
703
|
)
|
|
692
704
|
|
|
693
705
|
if self._state == AppSessionState.SHUTDOWN_REQUESTED:
|
|
694
|
-
# Only clear media files if the script is done
|
|
695
|
-
# session is actually shutting down.
|
|
706
|
+
# Only clear media files and session caches if the script is done
|
|
707
|
+
# running AND the session is actually shutting down.
|
|
696
708
|
runtime.get_instance().media_file_mgr.clear_session_refs(self.id)
|
|
709
|
+
self.clear_session_caches()
|
|
697
710
|
|
|
698
711
|
self._client_state = client_state
|
|
699
712
|
self._scriptrunner = None
|
|
@@ -21,12 +21,18 @@ from streamlit.runtime.caching.cache_data_api import (
|
|
|
21
21
|
CacheDataAPI,
|
|
22
22
|
get_data_cache_stats_provider,
|
|
23
23
|
)
|
|
24
|
+
from streamlit.runtime.caching.cache_data_api import (
|
|
25
|
+
clear_session_cache as clear_session_data_cache,
|
|
26
|
+
)
|
|
24
27
|
from streamlit.runtime.caching.cache_errors import CACHE_DOCS_URL
|
|
25
28
|
from streamlit.runtime.caching.cache_resource_api import (
|
|
26
29
|
CACHE_RESOURCE_MESSAGE_REPLAY_CTX,
|
|
27
30
|
CacheResourceAPI,
|
|
28
31
|
get_resource_cache_stats_provider,
|
|
29
32
|
)
|
|
33
|
+
from streamlit.runtime.caching.cache_resource_api import (
|
|
34
|
+
clear_session_cache as clear_session_resource_cache,
|
|
35
|
+
)
|
|
30
36
|
from streamlit.runtime.caching.legacy_cache_api import cache as _cache
|
|
31
37
|
|
|
32
38
|
if TYPE_CHECKING:
|
|
@@ -102,6 +108,8 @@ __all__ = [
|
|
|
102
108
|
"cache",
|
|
103
109
|
"cache_data",
|
|
104
110
|
"cache_resource",
|
|
111
|
+
"clear_session_data_cache",
|
|
112
|
+
"clear_session_resource_cache",
|
|
105
113
|
"get_data_cache_stats_provider",
|
|
106
114
|
"get_resource_cache_stats_provider",
|
|
107
115
|
"save_block_message",
|
|
@@ -41,6 +41,8 @@ from streamlit.runtime.caching.cache_utils import (
|
|
|
41
41
|
Cache,
|
|
42
42
|
CachedFunc,
|
|
43
43
|
CachedFuncInfo,
|
|
44
|
+
CacheScope,
|
|
45
|
+
get_session_id_or_throw,
|
|
44
46
|
make_cached_func_wrapper,
|
|
45
47
|
)
|
|
46
48
|
from streamlit.runtime.caching.cached_message_replay import (
|
|
@@ -104,12 +106,14 @@ class CachedDataFuncInfo(CachedFuncInfo[P, R]):
|
|
|
104
106
|
show_spinner: bool | str,
|
|
105
107
|
show_time: bool = False,
|
|
106
108
|
hash_funcs: HashFuncsDict | None = None,
|
|
109
|
+
scope: CacheScope = "global",
|
|
107
110
|
) -> None:
|
|
108
111
|
super().__init__(
|
|
109
112
|
func,
|
|
110
113
|
hash_funcs=hash_funcs,
|
|
111
114
|
show_spinner=show_spinner,
|
|
112
115
|
show_time=show_time,
|
|
116
|
+
scope=scope,
|
|
113
117
|
)
|
|
114
118
|
self.persist = persist
|
|
115
119
|
self.max_entries = max_entries
|
|
@@ -137,6 +141,7 @@ class CachedDataFuncInfo(CachedFuncInfo[P, R]):
|
|
|
137
141
|
max_entries=self.max_entries,
|
|
138
142
|
ttl=self.ttl,
|
|
139
143
|
display_name=self.display_name,
|
|
144
|
+
scope=self.scope,
|
|
140
145
|
)
|
|
141
146
|
|
|
142
147
|
def validate_params(self) -> None:
|
|
@@ -159,7 +164,8 @@ class DataCaches(StatsProvider):
|
|
|
159
164
|
|
|
160
165
|
def __init__(self) -> None:
|
|
161
166
|
self._caches_lock = threading.Lock()
|
|
162
|
-
|
|
167
|
+
# Map of session IDs to map of function keys to caches.
|
|
168
|
+
self._function_caches: dict[str | None, dict[str, DataCache[Any]]] = {}
|
|
163
169
|
|
|
164
170
|
@property
|
|
165
171
|
def stats_families(self) -> Sequence[str]:
|
|
@@ -172,18 +178,37 @@ class DataCaches(StatsProvider):
|
|
|
172
178
|
max_entries: int | None,
|
|
173
179
|
ttl: int | float | timedelta | str | None,
|
|
174
180
|
display_name: str,
|
|
181
|
+
scope: CacheScope = "global",
|
|
175
182
|
) -> DataCache[Any]:
|
|
176
183
|
"""Return the mem cache for the given key.
|
|
177
184
|
|
|
178
185
|
If it doesn't exist, create a new one with the given params.
|
|
186
|
+
|
|
187
|
+
Raises
|
|
188
|
+
------
|
|
189
|
+
StreamlitAPIException
|
|
190
|
+
Raised when ``scope`` is ``"session"`` and there is no thread-local run
|
|
191
|
+
context.
|
|
179
192
|
"""
|
|
180
193
|
|
|
181
194
|
ttl_seconds = time_to_seconds(ttl, coerce_none_to_inf=False)
|
|
182
195
|
|
|
196
|
+
# Fetch the session ID. Note that this will throw an exception if there is no
|
|
197
|
+
# session associated with the current thread.
|
|
198
|
+
session_id: str | None
|
|
199
|
+
if scope == "global":
|
|
200
|
+
session_id = None
|
|
201
|
+
else:
|
|
202
|
+
session_id = get_session_id_or_throw()
|
|
203
|
+
|
|
183
204
|
# Get the existing cache, if it exists, and validate that its params
|
|
184
205
|
# haven't changed.
|
|
185
206
|
with self._caches_lock:
|
|
186
|
-
|
|
207
|
+
session_caches = self._function_caches.get(session_id)
|
|
208
|
+
if session_caches is None:
|
|
209
|
+
session_caches = self._function_caches[session_id] = {}
|
|
210
|
+
|
|
211
|
+
cache = session_caches.get(key)
|
|
187
212
|
if (
|
|
188
213
|
cache is not None
|
|
189
214
|
and cache.ttl_seconds == ttl_seconds
|
|
@@ -232,9 +257,22 @@ class DataCaches(StatsProvider):
|
|
|
232
257
|
ttl_seconds=ttl_seconds,
|
|
233
258
|
display_name=display_name,
|
|
234
259
|
)
|
|
235
|
-
self._function_caches[key] = cache
|
|
260
|
+
self._function_caches[session_id][key] = cache
|
|
236
261
|
return cache
|
|
237
262
|
|
|
263
|
+
def clear_session(self, session_id: str) -> None:
|
|
264
|
+
"""Clears all caches for the given session ID."""
|
|
265
|
+
# Hold the lock while removing the cache, but release it while clearing.
|
|
266
|
+
with self._caches_lock:
|
|
267
|
+
session_caches = self._function_caches.get(session_id)
|
|
268
|
+
if session_caches is not None:
|
|
269
|
+
del self._function_caches[session_id]
|
|
270
|
+
|
|
271
|
+
if session_caches is not None:
|
|
272
|
+
for cache in session_caches.values():
|
|
273
|
+
cache.clear()
|
|
274
|
+
cache.storage.close()
|
|
275
|
+
|
|
238
276
|
def clear_all(self) -> None:
|
|
239
277
|
"""Clear all in-memory and on-disk caches."""
|
|
240
278
|
with self._caches_lock:
|
|
@@ -245,9 +283,10 @@ class DataCaches(StatsProvider):
|
|
|
245
283
|
# available storages one by one
|
|
246
284
|
self.get_storage_manager().clear_all()
|
|
247
285
|
except NotImplementedError:
|
|
248
|
-
for
|
|
249
|
-
data_cache.
|
|
250
|
-
|
|
286
|
+
for data_caches in self._function_caches.values():
|
|
287
|
+
for data_cache in data_caches.values():
|
|
288
|
+
data_cache.clear()
|
|
289
|
+
data_cache.storage.close()
|
|
251
290
|
self._function_caches = {}
|
|
252
291
|
|
|
253
292
|
def get_stats(
|
|
@@ -256,10 +295,14 @@ class DataCaches(StatsProvider):
|
|
|
256
295
|
with self._caches_lock:
|
|
257
296
|
# Shallow-clone our caches. We don't want to hold the global
|
|
258
297
|
# lock during stats-gathering.
|
|
259
|
-
function_caches =
|
|
298
|
+
function_caches = [
|
|
299
|
+
cache
|
|
300
|
+
for caches in self._function_caches.values()
|
|
301
|
+
for cache in caches.values()
|
|
302
|
+
]
|
|
260
303
|
|
|
261
304
|
stats: list[CacheStat] = []
|
|
262
|
-
for cache in function_caches
|
|
305
|
+
for cache in function_caches:
|
|
263
306
|
cache_stats = cache.get_stats()
|
|
264
307
|
for family_stats in cache_stats.values():
|
|
265
308
|
stats.extend(family_stats)
|
|
@@ -334,6 +377,11 @@ class DataCaches(StatsProvider):
|
|
|
334
377
|
_data_caches = DataCaches()
|
|
335
378
|
|
|
336
379
|
|
|
380
|
+
def clear_session_cache(session_id: str) -> None:
|
|
381
|
+
"""Clears all caches for the given session ID."""
|
|
382
|
+
_data_caches.clear_session(session_id)
|
|
383
|
+
|
|
384
|
+
|
|
337
385
|
def get_data_cache_stats_provider() -> StatsProvider:
|
|
338
386
|
"""Return the StatsProvider for all @st.cache_data functions."""
|
|
339
387
|
return _data_caches
|
|
@@ -377,6 +425,7 @@ class CacheDataAPI:
|
|
|
377
425
|
show_time: bool = False,
|
|
378
426
|
persist: CachePersistType | bool = None,
|
|
379
427
|
hash_funcs: HashFuncsDict | None = None,
|
|
428
|
+
scope: CacheScope = "global",
|
|
380
429
|
) -> Callable[[Callable[P, R]], CachedFunc[P, R]]: ...
|
|
381
430
|
|
|
382
431
|
def __call__(
|
|
@@ -389,6 +438,7 @@ class CacheDataAPI:
|
|
|
389
438
|
show_time: bool = False,
|
|
390
439
|
persist: CachePersistType | bool = None,
|
|
391
440
|
hash_funcs: HashFuncsDict | None = None,
|
|
441
|
+
scope: CacheScope = "global",
|
|
392
442
|
) -> CachedFunc[P, R] | Callable[[Callable[P, R]], CachedFunc[P, R]]:
|
|
393
443
|
return self._decorator(
|
|
394
444
|
func, # ty: ignore[invalid-argument-type]
|
|
@@ -398,6 +448,7 @@ class CacheDataAPI:
|
|
|
398
448
|
show_spinner=show_spinner,
|
|
399
449
|
show_time=show_time,
|
|
400
450
|
hash_funcs=hash_funcs,
|
|
451
|
+
scope=scope,
|
|
401
452
|
)
|
|
402
453
|
|
|
403
454
|
def _decorator(
|
|
@@ -410,6 +461,7 @@ class CacheDataAPI:
|
|
|
410
461
|
show_time: bool = False,
|
|
411
462
|
persist: CachePersistType | bool,
|
|
412
463
|
hash_funcs: HashFuncsDict | None = None,
|
|
464
|
+
scope: CacheScope = "global",
|
|
413
465
|
) -> CachedFunc[P, R] | Callable[[Callable[P, R]], CachedFunc[P, R]]:
|
|
414
466
|
"""Decorator to cache functions that return data (e.g. dataframe transforms, database queries, ML inference).
|
|
415
467
|
|
|
@@ -491,6 +543,15 @@ class CacheDataAPI:
|
|
|
491
543
|
the provided function to generate a hash for it. See below for an example
|
|
492
544
|
of how this can be used.
|
|
493
545
|
|
|
546
|
+
scope : "global" or "session"
|
|
547
|
+
The scope for the resource. If "global", cache globally. If "session",
|
|
548
|
+
cache in the session.
|
|
549
|
+
|
|
550
|
+
Session-scoped cache entries will be expired when a user's session is
|
|
551
|
+
disconnected. Note that disconnected sessions can reconnect - so it is
|
|
552
|
+
possible for the cache to populate multiple times in a single session for
|
|
553
|
+
the same key.
|
|
554
|
+
|
|
494
555
|
Example
|
|
495
556
|
-------
|
|
496
557
|
>>> import streamlit as st
|
|
@@ -595,6 +656,11 @@ class CacheDataAPI:
|
|
|
595
656
|
f"Unsupported persist option '{persist}'. Valid values are 'disk' or None."
|
|
596
657
|
)
|
|
597
658
|
|
|
659
|
+
if scope not in ("global", "session"):
|
|
660
|
+
raise StreamlitAPIException(
|
|
661
|
+
f"Unsupported scope option '{scope}'. Valid values are 'global' or 'session'."
|
|
662
|
+
)
|
|
663
|
+
|
|
598
664
|
def wrapper(f: Callable[P, R]) -> CachedFunc[P, R]:
|
|
599
665
|
return make_cached_func_wrapper(
|
|
600
666
|
CachedDataFuncInfo(
|
|
@@ -605,6 +671,7 @@ class CacheDataAPI:
|
|
|
605
671
|
max_entries=max_entries,
|
|
606
672
|
ttl=ttl,
|
|
607
673
|
hash_funcs=hash_funcs,
|
|
674
|
+
scope=scope,
|
|
608
675
|
)
|
|
609
676
|
)
|
|
610
677
|
|
|
@@ -620,6 +687,7 @@ class CacheDataAPI:
|
|
|
620
687
|
max_entries=max_entries,
|
|
621
688
|
ttl=ttl,
|
|
622
689
|
hash_funcs=hash_funcs,
|
|
690
|
+
scope=scope,
|
|
623
691
|
)
|
|
624
692
|
)
|
|
625
693
|
|
|
@@ -32,6 +32,7 @@ from typing import (
|
|
|
32
32
|
from typing_extensions import ParamSpec
|
|
33
33
|
|
|
34
34
|
import streamlit as st
|
|
35
|
+
from streamlit.errors import StreamlitAPIException
|
|
35
36
|
from streamlit.logger import get_logger
|
|
36
37
|
from streamlit.runtime.caching import cache_utils
|
|
37
38
|
from streamlit.runtime.caching.cache_errors import CacheKeyNotFoundError
|
|
@@ -40,7 +41,9 @@ from streamlit.runtime.caching.cache_utils import (
|
|
|
40
41
|
Cache,
|
|
41
42
|
CachedFunc,
|
|
42
43
|
CachedFuncInfo,
|
|
44
|
+
CacheScope,
|
|
43
45
|
OnRelease,
|
|
46
|
+
get_session_id_or_throw,
|
|
44
47
|
make_cached_func_wrapper,
|
|
45
48
|
)
|
|
46
49
|
from streamlit.runtime.caching.cached_message_replay import (
|
|
@@ -89,7 +92,8 @@ class ResourceCaches(StatsProvider):
|
|
|
89
92
|
|
|
90
93
|
def __init__(self) -> None:
|
|
91
94
|
self._caches_lock = threading.Lock()
|
|
92
|
-
|
|
95
|
+
# Map of session IDs to map of function keys to caches.
|
|
96
|
+
self._function_caches: dict[str | None, dict[str, ResourceCache[Any]]] = {}
|
|
93
97
|
|
|
94
98
|
@property
|
|
95
99
|
def stats_families(self) -> Sequence[str]:
|
|
@@ -103,20 +107,39 @@ class ResourceCaches(StatsProvider):
|
|
|
103
107
|
ttl: float | timedelta | str | None,
|
|
104
108
|
validate: ValidateFunc | None,
|
|
105
109
|
on_release: OnRelease,
|
|
110
|
+
scope: CacheScope = "global",
|
|
106
111
|
) -> ResourceCache[Any]:
|
|
107
112
|
"""Return the mem cache for the given key.
|
|
108
113
|
|
|
109
114
|
If it doesn't exist, create a new one with the given params.
|
|
115
|
+
|
|
116
|
+
Raises
|
|
117
|
+
------
|
|
118
|
+
StreamlitAPIException
|
|
119
|
+
Raised when ``scope`` is ``"session"`` and there is no thread-local run
|
|
120
|
+
context.
|
|
110
121
|
"""
|
|
111
122
|
if max_entries is None:
|
|
112
123
|
max_entries = math.inf
|
|
113
124
|
|
|
114
125
|
ttl_seconds = time_to_seconds(ttl)
|
|
115
126
|
|
|
127
|
+
# Fetch the session ID. Note that this will throw an exception if there is no
|
|
128
|
+
# session associated with the current thread.
|
|
129
|
+
session_id: str | None
|
|
130
|
+
if scope == "global":
|
|
131
|
+
session_id = None
|
|
132
|
+
else:
|
|
133
|
+
session_id = get_session_id_or_throw()
|
|
134
|
+
|
|
116
135
|
# Get the existing cache, if it exists, and validate that its params
|
|
117
136
|
# haven't changed.
|
|
118
137
|
with self._caches_lock:
|
|
119
|
-
|
|
138
|
+
session_caches = self._function_caches.get(session_id)
|
|
139
|
+
if session_caches is None:
|
|
140
|
+
session_caches = self._function_caches[session_id] = {}
|
|
141
|
+
|
|
142
|
+
cache = session_caches.get(key)
|
|
120
143
|
if (
|
|
121
144
|
cache is not None
|
|
122
145
|
and cache.ttl_seconds == ttl_seconds
|
|
@@ -135,14 +158,30 @@ class ResourceCaches(StatsProvider):
|
|
|
135
158
|
validate=validate,
|
|
136
159
|
on_release=on_release,
|
|
137
160
|
)
|
|
138
|
-
self._function_caches[key] = cache
|
|
161
|
+
self._function_caches[session_id][key] = cache
|
|
139
162
|
return cache
|
|
140
163
|
|
|
164
|
+
def clear_session(self, session_id: str) -> None:
|
|
165
|
+
"""Clears all caches for the given session ID."""
|
|
166
|
+
# Hold the lock while removing the cache, but release it while clearing.
|
|
167
|
+
with self._caches_lock:
|
|
168
|
+
session_caches = self._function_caches.get(session_id)
|
|
169
|
+
if session_caches is not None:
|
|
170
|
+
del self._function_caches[session_id]
|
|
171
|
+
|
|
172
|
+
if session_caches is not None:
|
|
173
|
+
for cache in session_caches.values():
|
|
174
|
+
cache.clear()
|
|
175
|
+
|
|
141
176
|
def clear_all(self) -> None:
|
|
142
177
|
"""Clear all resource caches."""
|
|
143
178
|
# Hold the lock long enough to copy the caches.
|
|
144
179
|
with self._caches_lock:
|
|
145
|
-
caches =
|
|
180
|
+
caches = [
|
|
181
|
+
cache
|
|
182
|
+
for caches in self._function_caches.values()
|
|
183
|
+
for cache in caches.values()
|
|
184
|
+
]
|
|
146
185
|
self._function_caches = {}
|
|
147
186
|
|
|
148
187
|
# Clear each cache to ensure any on_release functions are called.
|
|
@@ -152,13 +191,18 @@ class ResourceCaches(StatsProvider):
|
|
|
152
191
|
def get_stats(
|
|
153
192
|
self, _family_names: Sequence[str] | None = None
|
|
154
193
|
) -> dict[str, list[CacheStat]]:
|
|
194
|
+
function_caches: list[ResourceCache[Any]]
|
|
155
195
|
with self._caches_lock:
|
|
156
196
|
# Shallow-clone our caches. We don't want to hold the global
|
|
157
197
|
# lock during stats-gathering.
|
|
158
|
-
function_caches =
|
|
198
|
+
function_caches = [
|
|
199
|
+
cache
|
|
200
|
+
for caches in self._function_caches.values()
|
|
201
|
+
for cache in caches.values()
|
|
202
|
+
]
|
|
159
203
|
|
|
160
204
|
stats: list[CacheStat] = []
|
|
161
|
-
for cache in function_caches
|
|
205
|
+
for cache in function_caches:
|
|
162
206
|
cache_stats = cache.get_stats()
|
|
163
207
|
for family_stats in cache_stats.values():
|
|
164
208
|
stats.extend(family_stats)
|
|
@@ -174,6 +218,11 @@ class ResourceCaches(StatsProvider):
|
|
|
174
218
|
_resource_caches = ResourceCaches()
|
|
175
219
|
|
|
176
220
|
|
|
221
|
+
def clear_session_cache(session_id: str) -> None:
|
|
222
|
+
"""Clears all caches for the given session ID."""
|
|
223
|
+
_resource_caches.clear_session(session_id)
|
|
224
|
+
|
|
225
|
+
|
|
177
226
|
def get_resource_cache_stats_provider() -> StatsProvider:
|
|
178
227
|
"""Return the StatsProvider for all @st.cache_resource functions."""
|
|
179
228
|
return _resource_caches
|
|
@@ -196,12 +245,14 @@ class CachedResourceFuncInfo(CachedFuncInfo[P, R]):
|
|
|
196
245
|
hash_funcs: HashFuncsDict | None = None,
|
|
197
246
|
show_time: bool = False,
|
|
198
247
|
on_release: OnRelease | None = None,
|
|
248
|
+
scope: CacheScope = "global",
|
|
199
249
|
) -> None:
|
|
200
250
|
super().__init__(
|
|
201
251
|
func,
|
|
202
252
|
hash_funcs=hash_funcs,
|
|
203
253
|
show_spinner=show_spinner,
|
|
204
254
|
show_time=show_time,
|
|
255
|
+
scope=scope,
|
|
205
256
|
)
|
|
206
257
|
self.max_entries = max_entries
|
|
207
258
|
self.ttl = ttl
|
|
@@ -229,6 +280,7 @@ class CachedResourceFuncInfo(CachedFuncInfo[P, R]):
|
|
|
229
280
|
ttl=self.ttl,
|
|
230
281
|
validate=self.validate,
|
|
231
282
|
on_release=self.on_release,
|
|
283
|
+
scope=self.scope,
|
|
232
284
|
)
|
|
233
285
|
|
|
234
286
|
|
|
@@ -268,6 +320,8 @@ class CacheResourceAPI:
|
|
|
268
320
|
show_time: bool = False,
|
|
269
321
|
validate: ValidateFunc | None = None,
|
|
270
322
|
hash_funcs: HashFuncsDict | None = None,
|
|
323
|
+
on_release: OnRelease | None = None,
|
|
324
|
+
scope: CacheScope = "global",
|
|
271
325
|
) -> Callable[[Callable[P, R]], CachedFunc[P, R]]: ...
|
|
272
326
|
|
|
273
327
|
def __call__(
|
|
@@ -281,6 +335,7 @@ class CacheResourceAPI:
|
|
|
281
335
|
validate: ValidateFunc | None = None,
|
|
282
336
|
hash_funcs: HashFuncsDict | None = None,
|
|
283
337
|
on_release: OnRelease | None = None,
|
|
338
|
+
scope: CacheScope = "global",
|
|
284
339
|
) -> CachedFunc[P, R] | Callable[[Callable[P, R]], CachedFunc[P, R]]:
|
|
285
340
|
return self._decorator( # ty: ignore[missing-argument]
|
|
286
341
|
func, # ty: ignore[invalid-argument-type]
|
|
@@ -291,6 +346,7 @@ class CacheResourceAPI:
|
|
|
291
346
|
validate=validate,
|
|
292
347
|
hash_funcs=hash_funcs,
|
|
293
348
|
on_release=on_release,
|
|
349
|
+
scope=scope,
|
|
294
350
|
)
|
|
295
351
|
|
|
296
352
|
def _decorator(
|
|
@@ -304,6 +360,7 @@ class CacheResourceAPI:
|
|
|
304
360
|
validate: ValidateFunc | None,
|
|
305
361
|
hash_funcs: HashFuncsDict | None = None,
|
|
306
362
|
on_release: OnRelease | None = None,
|
|
363
|
+
scope: CacheScope = "global",
|
|
307
364
|
) -> CachedFunc[P, R] | Callable[[Callable[P, R]], CachedFunc[P, R]]:
|
|
308
365
|
"""Decorator to cache functions that return global resources (e.g. database connections, ML models).
|
|
309
366
|
|
|
@@ -415,6 +472,15 @@ class CacheResourceAPI:
|
|
|
415
472
|
|
|
416
473
|
This will NOT be called when an app is shut down.
|
|
417
474
|
|
|
475
|
+
scope : "global" or "session"
|
|
476
|
+
The scope for the resource. If "global", cache globally. If "session",
|
|
477
|
+
cache in the session.
|
|
478
|
+
|
|
479
|
+
Session-scoped cache entries will be expired when a user's session is
|
|
480
|
+
disconnected. Note that disconnected sessions can reconnect - so it is
|
|
481
|
+
possible for the cache to populate multiple times in a single session for
|
|
482
|
+
the same key.
|
|
483
|
+
|
|
418
484
|
Example
|
|
419
485
|
-------
|
|
420
486
|
>>> import streamlit as st
|
|
@@ -498,6 +564,11 @@ class CacheResourceAPI:
|
|
|
498
564
|
... return person.name
|
|
499
565
|
"""
|
|
500
566
|
|
|
567
|
+
if scope not in ("global", "session"):
|
|
568
|
+
raise StreamlitAPIException(
|
|
569
|
+
f"Unsupported scope option '{scope}'. Valid values are 'global' or 'session'."
|
|
570
|
+
)
|
|
571
|
+
|
|
501
572
|
# Support passing the params via function decorator, e.g.
|
|
502
573
|
# @st.cache_resource(show_spinner=False)
|
|
503
574
|
if func is None:
|
|
@@ -511,6 +582,7 @@ class CacheResourceAPI:
|
|
|
511
582
|
validate=validate,
|
|
512
583
|
hash_funcs=hash_funcs,
|
|
513
584
|
on_release=on_release,
|
|
585
|
+
scope=scope,
|
|
514
586
|
)
|
|
515
587
|
)
|
|
516
588
|
|
|
@@ -524,6 +596,7 @@ class CacheResourceAPI:
|
|
|
524
596
|
validate=validate,
|
|
525
597
|
hash_funcs=hash_funcs,
|
|
526
598
|
on_release=on_release,
|
|
599
|
+
scope=scope,
|
|
527
600
|
)
|
|
528
601
|
)
|
|
529
602
|
|
|
@@ -30,6 +30,7 @@ from typing import (
|
|
|
30
30
|
Any,
|
|
31
31
|
Final,
|
|
32
32
|
Generic,
|
|
33
|
+
Literal,
|
|
33
34
|
TypeAlias,
|
|
34
35
|
TypeVar,
|
|
35
36
|
cast,
|
|
@@ -41,6 +42,7 @@ from typing_extensions import ParamSpec
|
|
|
41
42
|
from streamlit import type_util
|
|
42
43
|
from streamlit.dataframe_util import is_unevaluated_data_object
|
|
43
44
|
from streamlit.delta_generator_singletons import get_dg_singleton_instance
|
|
45
|
+
from streamlit.errors import StreamlitAPIException
|
|
44
46
|
from streamlit.logger import get_logger
|
|
45
47
|
from streamlit.runtime.caching.cache_errors import (
|
|
46
48
|
CacheError,
|
|
@@ -81,6 +83,32 @@ R = TypeVar("R")
|
|
|
81
83
|
OnRelease: TypeAlias = Callable[[Any], None]
|
|
82
84
|
|
|
83
85
|
|
|
86
|
+
# The scope of a cache.
|
|
87
|
+
CacheScope: TypeAlias = Literal["global", "session"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def get_session_id_or_throw() -> str:
|
|
91
|
+
"""Returns the active session ID from the thread-local run context.
|
|
92
|
+
|
|
93
|
+
Raises
|
|
94
|
+
------
|
|
95
|
+
StreamlitAPIException
|
|
96
|
+
Raised if there is no thread-local run context.
|
|
97
|
+
"""
|
|
98
|
+
from streamlit.runtime.scriptrunner_utils.script_run_context import (
|
|
99
|
+
get_script_run_ctx,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
ctx = get_script_run_ctx()
|
|
103
|
+
if ctx is None:
|
|
104
|
+
raise StreamlitAPIException(
|
|
105
|
+
"A session-scoped cache was accessed outside of the app execution thread. "
|
|
106
|
+
"Make sure all session-scoped caches are read during rendering and not "
|
|
107
|
+
"read in background threads."
|
|
108
|
+
)
|
|
109
|
+
return ctx.session_id
|
|
110
|
+
|
|
111
|
+
|
|
84
112
|
class Cache(Generic[R]):
|
|
85
113
|
"""Function cache interface. Caches persist across script runs."""
|
|
86
114
|
|
|
@@ -96,7 +124,9 @@ class Cache(Generic[R]):
|
|
|
96
124
|
------
|
|
97
125
|
CacheKeyNotFoundError
|
|
98
126
|
Raised if value_key is not in the cache.
|
|
99
|
-
|
|
127
|
+
StreamlitAPIException
|
|
128
|
+
Raised when a thread attempts to read from a session-scoped cache but that
|
|
129
|
+
thread does not have a session associated with it.
|
|
100
130
|
"""
|
|
101
131
|
raise NotImplementedError
|
|
102
132
|
|
|
@@ -104,6 +134,12 @@ class Cache(Generic[R]):
|
|
|
104
134
|
def write_result(self, value_key: str, value: R, messages: list[MsgData]) -> None:
|
|
105
135
|
"""Write a value and associated messages to the cache, overwriting any existing
|
|
106
136
|
result that uses the value_key.
|
|
137
|
+
|
|
138
|
+
Raises
|
|
139
|
+
------
|
|
140
|
+
StreamlitAPIException
|
|
141
|
+
Raised when a thread attempts to write to a session-scoped cache but that
|
|
142
|
+
thread does not have a session associated with it.
|
|
107
143
|
"""
|
|
108
144
|
# We *could* `del self._value_locks[value_key]` here, since nobody will be taking
|
|
109
145
|
# a compute_value_lock for this value_key after the result is written.
|
|
@@ -150,11 +186,13 @@ class CachedFuncInfo(Generic[P, R]):
|
|
|
150
186
|
hash_funcs: HashFuncsDict | None,
|
|
151
187
|
show_spinner: bool | str,
|
|
152
188
|
show_time: bool = False,
|
|
189
|
+
scope: CacheScope = "global",
|
|
153
190
|
) -> None:
|
|
154
191
|
self.func = func
|
|
155
192
|
self.hash_funcs = hash_funcs
|
|
156
193
|
self.show_spinner = show_spinner
|
|
157
194
|
self.show_time = show_time
|
|
195
|
+
self.scope = scope
|
|
158
196
|
|
|
159
197
|
@property
|
|
160
198
|
def cache_type(self) -> CacheType:
|
|
@@ -165,7 +203,10 @@ class CachedFuncInfo(Generic[P, R]):
|
|
|
165
203
|
raise NotImplementedError
|
|
166
204
|
|
|
167
205
|
def get_function_cache(self, function_key: str) -> Cache[R]:
|
|
168
|
-
"""Get or create the function cache for the given key.
|
|
206
|
+
"""Get or create the function cache for the given key.
|
|
207
|
+
|
|
208
|
+
This is responsible for handling cache scope correctly.
|
|
209
|
+
"""
|
|
169
210
|
raise NotImplementedError
|
|
170
211
|
|
|
171
212
|
|
streamlit/runtime/stats.py
CHANGED
|
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, Final, NamedTuple, Protocol, runtime_checkable
|
|
|
19
19
|
|
|
20
20
|
CACHE_MEMORY_FAMILY: Final = "cache_memory_bytes"
|
|
21
21
|
SESSION_EVENTS_FAMILY: Final = "session_events_total"
|
|
22
|
+
SESSION_DURATION_FAMILY: Final = "session_duration_seconds_total"
|
|
22
23
|
ACTIVE_SESSIONS_FAMILY: Final = "active_sessions"
|
|
23
24
|
|
|
24
25
|
if TYPE_CHECKING:
|