-
-
Save tvst/036da038ab3e999a64497f42de966a92 to your computer and use it in GitHub Desktop.
"""Hack to add per-session state to Streamlit. | |
Usage | |
----- | |
>>> import SessionState | |
>>> | |
>>> session_state = SessionState.get(user_name='', favorite_color='black') | |
>>> session_state.user_name | |
'' | |
>>> session_state.user_name = 'Mary' | |
>>> session_state.favorite_color | |
'black' | |
Since you set user_name above, next time your script runs this will be the | |
result: | |
>>> session_state = get(user_name='', favorite_color='black') | |
>>> session_state.user_name | |
'Mary' | |
""" | |
try: | |
import streamlit.ReportThread as ReportThread | |
from streamlit.server.Server import Server | |
except Exception: | |
# Streamlit >= 0.65.0 | |
import streamlit.report_thread as ReportThread | |
from streamlit.server.server import Server | |
class SessionState(object): | |
def __init__(self, **kwargs): | |
"""A new SessionState object. | |
Parameters | |
---------- | |
**kwargs : any | |
Default values for the session state. | |
Example | |
------- | |
>>> session_state = SessionState(user_name='', favorite_color='black') | |
>>> session_state.user_name = 'Mary' | |
'' | |
>>> session_state.favorite_color | |
'black' | |
""" | |
for key, val in kwargs.items(): | |
setattr(self, key, val) | |
def get(**kwargs): | |
"""Gets a SessionState object for the current session. | |
Creates a new object if necessary. | |
Parameters | |
---------- | |
**kwargs : any | |
Default values you want to add to the session state, if we're creating a | |
new one. | |
Example | |
------- | |
>>> session_state = get(user_name='', favorite_color='black') | |
>>> session_state.user_name | |
'' | |
>>> session_state.user_name = 'Mary' | |
>>> session_state.favorite_color | |
'black' | |
Since you set user_name above, next time your script runs this will be the | |
result: | |
>>> session_state = get(user_name='', favorite_color='black') | |
>>> session_state.user_name | |
'Mary' | |
""" | |
# Hack to get the session object from Streamlit. | |
ctx = ReportThread.get_report_ctx() | |
this_session = None | |
current_server = Server.get_current() | |
if hasattr(current_server, '_session_infos'): | |
# Streamlit < 0.56 | |
session_infos = Server.get_current()._session_infos.values() | |
else: | |
session_infos = Server.get_current()._session_info_by_id.values() | |
for session_info in session_infos: | |
s = session_info.session | |
if ( | |
# Streamlit < 0.54.0 | |
(hasattr(s, '_main_dg') and s._main_dg == ctx.main_dg) | |
or | |
# Streamlit >= 0.54.0 | |
(not hasattr(s, '_main_dg') and s.enqueue == ctx.enqueue) | |
or | |
# Streamlit >= 0.65.2 | |
(not hasattr(s, '_main_dg') and s._uploaded_file_mgr == ctx.uploaded_file_mgr) | |
): | |
this_session = s | |
if this_session is None: | |
raise RuntimeError( | |
"Oh noes. Couldn't get your Streamlit Session object. " | |
'Are you doing something fancy with threads?') | |
# Got the session object! Now let's attach some state into it. | |
if not hasattr(this_session, '_custom_session_state'): | |
this_session._custom_session_state = SessionState(**kwargs) | |
return this_session._custom_session_state |
For anyone running into errors with this in what are currently nightly release versions, making these changes should fix it:
from streamlit.server.Server import Server
should change to
from streamlit.server.server import Server
and
import streamlit.ReportThread as ReportThread
should change to
import streamlit.report_thread as ReportThread
I tried to upgrade to 0.65.1 and run streamlit. When trying to work with the posted SessionState.py i just keep running into:
RuntimeError: Oh noes. Couldn't get your Streamlit Session objectAre you doing something fancy with threads?
this_session keeps coming up as None.
Even in the simplest scenario:
import streamlit as st
import st_state_patchv2
test = st_state_patchv2.get()
I cleand out all streamlit install in virtual envs or on system level and did a clean install of 0.65.1 including a force-reinstall with pip.
I also installed in Docker to make sure of clean environment
streamlit-nightly==0.64.1.dev20200805 -> worked
streamlit==0.65.1 -> failed
Any ideas?
It seems like this logic doesnt work anymore with the newest version:
if (
# Streamlit < 0.54.0
(hasattr(s, '_main_dg') and s._main_dg == ctx.main_dg)
or
# Streamlit >= 0.54.0
(not hasattr(s, '_main_dg') and s.enqueue == ctx.enqueue)
):
this_session = s
Seems like one now just has to compare on the actual ids in session_info / ctx to get the current session_id of the thread?
for session_info in session_infos:
s = session_info.session
if s.id == ctx.session_id:
this_session = s
Even quicker if you don't plan to use Streamlit < 0.56, instead of looping through session infos, there's a private method retrieving the correct session_info object based on the report thread's session id. I've been using the following snippet for some time, and it worked before and after 0.65 update.
def _get_session():
session_id = get_report_ctx().session_id
session_info = Server.get_current()._get_session_info(session_id)
if session_info is None:
raise RuntimeError("Couldn't get your Streamlit Session object.")
return session_info.session
Even quicker if you don't plan to use Streamlit < 0.56, instead of looping through session infos, there's a private method retrieving the correct session_info object based on the report thread's session id. I've been using the following snippet for some time, and it worked before and after 0.65 update.
def _get_session(): session_id = get_report_ctx().session_id session_info = Server.get_current()._get_session_info(session_id) if session_info is None: raise RuntimeError("Couldn't get your Streamlit Session object.") return session_info.session
Thanks @Ghasel, this works fine in my case, while the current method based on this comparison:
(not hasattr(s, '_main_dg') and s._uploaded_file_mgr == ctx.uploaded_file_mgr)
was not working with Streamlit -0.65.2, causing the same state to be retrieved from different sessions with cross-talk issues.
Is there an updated version of this for 0.66.0? I'm seeing this error:
Here are the import statements in my SessionState.py:
import streamlit.report_thread as ReportThread
from streamlit.server.server import Server
and my app just looks like this:
import SessionState
import streamlit as st
session = SessionState.get(a=5)
st.write(session.a)
Ignore all this, I didn't catch the updates in the gist itself. Those updates fix it. Thanks all!
Thanks @tvst for this nice piece of code 👍 and @Ghasel for the code snippet 👏!
With the original code I had problems with interfering sessions, so I forked this Gist to work with Streamlit >= 0.65:
https://gist.github.com/FranzDiebold/898396a6be785d9b5ca6f3706ef9b0bc
I wish this would just make it into a stable API already :P
Awesome work and thank you for implementing this!
Just to verify, if I create a multi-user streamlit app running on a server with several users using it concurrently, will the session state be unique to each user? I have gotten it to work in my test environment but I'm not sure what needs to change in production
I know that the session state is supposed to be unique to the browser session but when I access the same app running on localhost using two browser windows concurrently(different browsers), one stream of data precessing gets entangled in the other, which would be an issue in production.
I was having trouble using this implementation to get the correct session. I have a simple login/logout need for the streamlit dashboard with multiple users. While using the SessionState to remember which user is logged in, the session was always incorrectly identified and the login user was always overwritten by the latest logged user. Which was pretty problematic.
I solved it by using st.cache
on the context session id. If anyone is interested, this is my SessionState.py
; the interface works the exact same way (i.e. SessionState.get(variable_name=variable_value)
), but this at least does not interfere with other users using the dashboard.
from streamlit.report_thread import get_report_ctx
import streamlit as st
class SessionState(object):
def __init__(self, **kwargs):
"""A new SessionState object.
Parameters
----------
**kwargs : any
Default values for the session state.
Example
-------
>>> session_state = SessionState(user_name='', favorite_color='black')
>>> session_state.user_name = 'Mary'
''
>>> session_state.favorite_color
'black'
"""
for key, val in kwargs.items():
setattr(self, key, val)
@st.cache(allow_output_mutation=True)
def get_session(id, **kwargs):
return SessionState(**kwargs)
def get(**kwargs):
"""Gets a SessionState object for the current session.
Creates a new object if necessary.
Parameters
----------
**kwargs : any
Default values you want to add to the session state, if we're creating a
new one.
Example
-------
>>> session_state = get(user_name='', favorite_color='black')
>>> session_state.user_name
''
>>> session_state.user_name = 'Mary'
>>> session_state.favorite_color
'black'
Since you set user_name above, next time your script runs this will be the
result:
>>> session_state = get(user_name='', favorite_color='black')
>>> session_state.user_name
'Mary'
"""
ctx = get_report_ctx()
id = ctx.session_id
return get_session(id, **kwargs)
The main drawback of this method is in case the cache is cleared, the state is lost completely. So it's not great but at least works for different users just fine.
I lost track of this thread a while ago, but I think I managed to get session state to do the trick for me using FranzDiebold's alternative implementation. Will update once I get this finally running on a server.
I wish this would just make it into a stable API already :P
We're working on it right now, actually! There are a few more details we need to nail down, plus a lot of testing, so my guess is we're looking at a January release.
@tvst this is incredible. I know this is not yet production ready, but I'm having trouble explaining why this works to my colleagues. Could you please explain how/why this works at least on a high level?
Thanks in advance.
Thanks @tvst for this nice piece of code and @Ghasel for the code snippet !
With the original code I had problems with interfering sessions, so I forked this Gist to work with Streamlit >= 0.65:https://gist.github.com/FranzDiebold/898396a6be785d9b5ca6f3706ef9b0bc
Thanks! Please merge this fix in the gist :]
https://gist.github.com/FranzDiebold/898396a6be785d9b5ca6f3706ef9b0bc#file-sessionstate-py-L82
session_id = ReportThread.get_report_ctx().session_id
I was having trouble using this implementation to get the correct session. I have a simple login/logout need for the streamlit dashboard with multiple users. While using the SessionState to remember which user is logged in, the session was always incorrectly identified and the login user was always overwritten by the latest logged user. Which was pretty problematic.
I solved it by using
st.cache
on the context session id. If anyone is interested, this is mySessionState.py
; the interface works the exact same way (i.e.SessionState.get(variable_name=variable_value)
), but this at least does not interfere with other users using the dashboard.from streamlit.report_thread import get_report_ctx import streamlit as st class SessionState(object): def __init__(self, **kwargs): """A new SessionState object. Parameters ---------- **kwargs : any Default values for the session state. Example ------- >>> session_state = SessionState(user_name='', favorite_color='black') >>> session_state.user_name = 'Mary' '' >>> session_state.favorite_color 'black' """ for key, val in kwargs.items(): setattr(self, key, val) @st.cache(allow_output_mutation=True) def get_session(id, **kwargs): return SessionState(**kwargs) def get(**kwargs): """Gets a SessionState object for the current session. Creates a new object if necessary. Parameters ---------- **kwargs : any Default values you want to add to the session state, if we're creating a new one. Example ------- >>> session_state = get(user_name='', favorite_color='black') >>> session_state.user_name '' >>> session_state.user_name = 'Mary' >>> session_state.favorite_color 'black' Since you set user_name above, next time your script runs this will be the result: >>> session_state = get(user_name='', favorite_color='black') >>> session_state.user_name 'Mary' """ ctx = get_report_ctx() id = ctx.session_id return get_session(id, **kwargs)The main drawback of this method is in case the cache is cleared, the state is lost completely. So it's not great but at least works for different users just fine.
Thanks! it worked
So did someone solved this problem incase the cache is cleared
I lost track of this thread a while ago, but I think I managed to get session state to do the trick for me using FranzDiebold's alternative implementation. Will update once I get this finally running on a server.
Can you please post your solution
import streamlit as st
import streamlit.report_thread as ReportThread
from streamlit.server.server import Server
class SessionState(object):
def __init__(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
def get(**kwargs):
ctx = ReportThread.get_report_ctx()
this_session = None
current_server = Server.get_current()
if hasattr(current_server, '_session_infos'):
# Streamlit < 0.56
session_infos = Server.get_current()._session_infos.values()
else:
session_infos = Server.get_current()._session_info_by_id.values()
for session_info in session_infos:
s = session_info.session
if (not hasattr(s, '_main_dg') and s._uploaded_file_mgr == ctx.uploaded_file_mgr)):
this_session = s
if this_session is None:
raise RuntimeError(
"Oh noes. Couldn't get your Streamlit Session object. "
'Are you doing something fancy with threads?')
# Got the session object!
if not hasattr(this_session, 'my_state'):
this_session.my_state = {}
else :
print('####',str(ctx.session_id),this_session.my_state)
if str(ctx.session_id) not in this_session.my_state.keys():
this_session.my_state[str(ctx.session_id)] = SessionState(**kwargs)
return this_session.my_state[str(ctx.session_id)]
I tried this and assuming that this_session is kind of global variable across threads so I am creating attritubute my_state and initialising it with dictionary keep keys as ctx.session_id ? Is this correct way it's working though but I am not sure correctness
@francesfeng @ryshoooo @tvst I tried like this instead of the cache thing , so can some one please confirm it's correctness , for some reason this_session is behaving as global variable
import streamlit.report_thread as ReportThread
from streamlit.server.server import Server
class SessionState(object):
def __init__(self, **kwargs):
for key, val in kwargs.items():
setattr(self, key, val)
def get(**kwargs):
ctx = ReportThread.get_report_ctx()
current_server = Server.get_current()
if not hasattr(current_server, 'my_state'):
current_server.my_state = {}
if str(ctx.session_id) not in current_server.my_state.keys():
current_server.my_state[str(ctx.session_id)] = SessionState(**kwargs)
return current_server.my_state[str(ctx.session_id)]
I made a hack by adding attribute to current server object , hope it is okay or will I get some performance issues in futute? @tvst
There is another gist related to sessions in Streamlit here.
(( I decided to remove this reference. Better to see the following message from @okld ))
I used this implementation from distinct browsers without issues. However I am not using in production, so I don't know anything about performance, memory, etc.
Hope this helps...
The main goal of that session state you shared is to fix a rollback issue when you try to synchronize widget values with session state values.
Here's my original post on this implementation and how to use it: https://discuss.streamlit.io/t/multi-page-app-with-session-state/3074
One caveat with it though, if you use it with widgets, you won't be able to store objects that Streamlit cannot hash.
Soon we'll get an official session state implementation which's going to be way better than what we have for now.
Meanwhile, in case anyone want a simple implementation which supports setting default values, but don't need to sync widget values with session state:
from streamlit.report_thread import get_report_ctx
from streamlit.server.server import Server
from types import SimpleNamespace
def get_state(**default_values):
session_id = get_report_ctx().session_id
session_info = Server.get_current()._get_session_info(session_id)
if session_info is None:
raise RuntimeError("Could not get your Streamlit session instance.")
if not hasattr(session_info.session, "_custom_state"):
session_info.session._custom_state = SimpleNamespace(**default_values)
return session_info.session._custom_state
# Retrieve state and define default values once
state = get_state(my_default_number=2, my_default_string="hello")
# Assign a value to your state every run
state.my_value = "world"
print(
state.my_default_number,
state.my_default_string,
state.my_value
)
Thanks !!
Let me ask... It seems you are the original author of the code but... are you the same person of the reference I've shared ?
When I first searched for something about sessions in Streamlit, I have found Thiago's implementation and the other.
Thiago's implementation didn't work when two users opened the application, so I took the other one.
By the way, I have tested the code you shared above, using three users and it worked perfectly (thanks again).
I'm someone else
I'm not sure if it was discussed before. But when I use your @tvst SessionState the cached value is shared among multiple opened web pages or users. The change caused by one user will be visible to other users. How can we make SessionState to maintain the values per user. You can verify this by opening up two web pages with a sider value cached by a session state object. Change of slider value in page A will be visible in page B whenever it refreshes. (cc: @okld)
If you want to store regular values in your session state, you can try the code snippet above.
If you want to bind widget values with session state, try this implementation instead.
If you want to store regular values in your session state, you can try the code snippet above.
If you want to bind widget values with session state, try this implementation instead.
Thanks a lot! that works! @okld
As @okld said, you should not use this Gist anymore! It has been replaced by an official feature of Streamlit, called st.session_state 🥳
@tvst how to make session_state save to disk ,and recover state when re-run?
This is lovely! I have dynamic sliders in my app (meaning that they change based on a certain user input), and I used this solution to preserve slider values!