Saving data in local storage via streamlit

Hi, I was wondering if there was a way to store some fields in the browser local storage of a client and access them through streamlit. I want to be able to repopulate certain fields if the user closes my form and opens it after some time.

If this isn’t feasible is using cookies my best option and is there a good way of setting and updating cookie keys via streamlit?

Hi @pramodith, welcome to the forum. There’s st.experimental_set_query_params and st.experimental_get_query_params that allows you to send field information via query parameters in the browser’s URL bar.

More info here:

2 Likes

Hey @dataprofessor thanks for the pointer but I’m afraid I’m looking for something else. In my application the user should be able to close the url to my application, but on returning certain fields of the user need to be restored. The application does not have a login feature so we want to store these fields in the clients browser via local storage or through cookies.

2 Likes

Hi @pramodith

Would this Streamlit component be useful? It’s intended to help you manage your client’s cookies streamlit-cookies-manager · PyPI

Let us know! :relaxed:

3 Likes

Sir, I had a similar doubt. Can these be used for this? Or is there another way? I have not deployed my app yet, if I deploy it, will I be able to keep track of some ss variables from all the open instances at a central location?

1 Like

Hi @arnaud I’ve tried using this module but I keep running into an error. I’ve written this simple function to update any of the keys of the cookie.

def update_cookie_state(key, value):
    cookie_manager_obj = get_manager()
    tomorrow = datetime.datetime.now() + datetime.timedelta()
    if cookie_manager_obj.get(key):
        cookie_manager_obj.delete(key)
    cookie_manager_obj.set(key, value, expires_at=datetime.datetime(year=tomorrow.year, month=tomorrow.month, day=tomorrow.day))

However, when I try running this I get the following error :

streamlit.errors.DuplicateWidgetID: There are multiple identical st.extra_streamlit_components.CookieManager.cookie_manager widgets with
key='set'.
To fix this, please make sure that the key argument is unique for
each st.extra_streamlit_components.CookieManager.cookie_manager you create.

I’m not sure which key is not unique in this case.

1 Like

@Ishan_Mistry I answered in your thread, I think a fairly common solution to your problem is a database!

1 Like

Hey @pramodith

I’ve just built a synchronous way to access localStorage from Streamlit using websockets (streamlit-ws-localstorage), feel free to try it out.

I struggled for a couple of days with localStorage access (and authentication), and thought it would be easier to build a websocket based synchronous communication itself. The code is simple, just import the module and use it like this:

import streamlit as st
from streamlit_ws_localstorage import injectWebsocketCode, getOrCreateUID

# Main call to the api, returns a communication object
conn = injectWebsocketCode(hostPort='linode.liquidco.in', uid=getOrCreateUID())

st.write('setting into localStorage')
ret = conn.setLocalStorageVal(key='k1', val='v1')
st.write('ret: ' + ret)

st.write('getting from localStorage')
ret = conn.getLocalStorageVal(key='k1')
st.write('ret: ' + ret)

Here is a demo of fetching saved info in the browser:

Installation: pip install streamlit-ws-localstorage
Repository: GitHub - gagangoku/streamlit-ws-localstorage: A simple synchronous way of accessing localStorage from your Streamlit app.
On pypi: streamlit-ws-localstorage · PyPI

4 Likes

the “key” means the wiget_id ,not key in dict. when you use the wiget like get,set,delete, you should not use same Key, you can apply like this:key=“0”, “1”,“2”,etc,it works

2 Likes

I tried your code verbatim. But it throws an error:

ssl.SSLCertVerificationError: This app has encountered an error. The original error message is redacted to prevent data leaks. Full error details have been recorded in the logs (if you're on Streamlit Cloud, click on 'Manage app' in the lower right of your app).
Traceback:
File "/home/adminuser/venv/lib/python3.9/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 535, in _run_script
    exec(code, module.__dict__)
File "/mount/src/xxxxx/xxxxx.py", line 8, in <module>
    ret = conn.setLocalStorageVal(key='k1', val='v1')
File "/home/adminuser/venv/lib/python3.9/site-packages/streamlit_ws_localstorage/__init__.py", line 99, in setLocalStorageVal
    result = self.sendCommand(json.dumps({ 'cmd': 'localStorage_set_key', 'key': key, 'val': val }))
File "/home/adminuser/venv/lib/python3.9/site-packages/streamlit_ws_localstorage/__init__.py", line 90, in sendCommand
    self.loop.run_until_complete(query(future1))
File "/usr/local/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
    return future.result()
File "/home/adminuser/venv/lib/python3.9/site-packages/streamlit_ws_localstorage/__init__.py", line 80, in query
    async with websockets.connect("wss://" + self.hostPort + "/?uid=" + self.uid, ssl=ssl_context) as ws:
File "/home/adminuser/venv/lib/python3.9/site-packages/websockets/legacy/client.py", line 629, in __aenter__
    return await self
File "/home/adminuser/venv/lib/python3.9/site-packages/websockets/legacy/client.py", line 647, in __await_impl_timeout__
    return await self.__await_impl__()
File "/home/adminuser/venv/lib/python3.9/site-packages/websockets/legacy/client.py", line 651, in __await_impl__
    _transport, _protocol = await self._create_connection()
File "/usr/local/lib/python3.9/asyncio/base_events.py", line 1090, in create_connection
    transport, protocol = await self._create_connection_transport(
File "/usr/local/lib/python3.9/asyncio/base_events.py", line 1120, in _create_connection_transport
    await waiter
File "/usr/local/lib/python3.9/asyncio/sslproto.py", line 534, in data_received
    ssldata, appdata = self._sslpipe.feed_ssldata(data)
File "/usr/local/lib/python3.9/asyncio/sslproto.py", line 188, in feed_ssldata
    self._sslobj.do_handshake()
File "/usr/local/lib/python3.9/ssl.py", line 945, in do_handshake
    self._sslobj.do_handshake()

How did you overcome the SSL certificates error?

1 Like

Hey @DevBhuyan

Can you try it with wsauthserver.supergroup.ai, recently switched servers.

1 Like

If I understand correctly, this solution requires a second server to be running somewhere. Is that correct?

1 Like

Yes @shawngiese

streamlit-ws-localstorage/streamlit_ws_localstorage/auth_redirect_server at main · gagangoku/streamlit-ws-localstorage · GitHub has the code for the auth redirect server.

You can host your own, or use wsauthserver.supergroup.ai.

2 Likes

I used streamlit-javascript ibrary to handle it:

from streamlit_javascript import st_javascript

def local_storage_get(key):
    return st_javascript(f"localStorage.getItem('{key}');")

def local_storage_set(key, value):
    value = json.dumps(value, ensure_ascii=False)
    return st_javascript(f"localStorage.setItem('{key}', JSON.stringify('{value}');")

The only problem was, that due to asynchronous nature of streamlit components, st_javascript did not provide return value of js code immidiatelly. Instead, it reloads some number of times before providing the value. And in case of None, you just don’t know if it returns empty value because you js code returns null or because the code execution hasn’t finished yet. So, the following code didn’t work

if "token" not in st.session_state:
    st.session_state.token = local_storage_get("token")

It just saved 0 - initial value of st_javascript component.


So, i build my own version of this lib, that allows distinguishing between “not ready yet” and “returns none” states.

Feel free to check it out here

Brief explanation:
It returns [] if the code execution is in progress and [] if it finishes. So, you can just st.stop() until result is not empty or for unblocking case, just check it before setting session_state value

if "token" not in st.session_state:
    if result := local_storage_get("token"): 
        st.session_state.token = result[0]
2 Likes

Hey @toolittlecakes I wanted to try this out but both links in your post seem to go to the original repo? is there a link to your fork somewhere? Thanks!

EDIT: Nvm, i found it from guessing your github username :smile: for anyone else curious:

Based on the work of @toolittlecakes and the streamlit_js library, I wrote a small gist for an st_local_storage that abstracts reading and writing keys to local storage to a dict / session_state like interface. Let me know how it is if you try it out!

1 Like

Thanks for creating this, Joshua!

This works pretty well when saving one value to local storage.
I gave it a try even within the same file you had shared. It look like it saves the value for the first key but not the second key.

Any idea why this may occur?

import json
from typing import Any
import uuid
import streamlit as st
from streamlit_js import st_js_blocking

KEY_PREFIX = "st_localstorage_"

# Keep track of a UUID for each key to enable reruns
if "_ls_unique_keys" not in st.session_state:
    st.session_state["_ls_unique_keys"] = {}
_ls_keys = st.session_state["_ls_unique_keys"]

class StLocalStorage:
    """An Dict-like wrapper around browser local storage.
    
    Values are stored JSON encoded."""

    def __getitem__(self, key: str) -> Any:
        if key not in _ls_keys:
            _ls_keys[key] = str(uuid.uuid4())
        code = f"""
        // The UUID changes on save, which causes this to rerun and update
        console.debug('{_ls_keys[key]}');
        return JSON.parse(localStorage.getItem('{KEY_PREFIX + key}'));
        """
        result = st_js_blocking(code)
        if result:
            return json.loads(result)
        return None

    def __setitem__(self, key: str, value: Any) -> None:
        value = json.dumps(value, ensure_ascii=False)
        _ls_keys[key] = str(uuid.uuid4())
        code = f"localStorage.setItem('{KEY_PREFIX + key}', JSON.stringify('{value}'));"
        return st_js_blocking(code)

    def __delitem__(self, key: str) -> None:
        _ls_keys[key] = str(uuid.uuid4())
        code = f"localStorage.removeItem('{KEY_PREFIX + key}');"
        return st_js_blocking(code)    

    def __contains__(self, key: str) -> bool:
        val = self.__getitem__(key)
        if val:
            return True
        return False


st_local_storage = StLocalStorage()

if __name__ == "__main__":
    st.title("st_local_storage basic example")

    "Any values you save will be available after leaving / refreshing the tab"

    key = st.text_input("Key")
    value = st.text_input("Value")
    test_key = st.text_input("TestKey") # new test key to save after first key

    if st.button("Save"):
        st_local_storage[key] = value
        st_local_storage[test_key] = value + " test"

    if key:
        st.write(f"Current value of {key} is: {st_local_storage[key]}")
        st.write(f"Current value of {test_key} is: {st_local_storage[test_key]}")

    if st.button("Delete"):
        del st_local_storage[key]
        st.rerun()

Hey @dkn-vtl thanks for reporting that, I hadn’t seen it but I do hit the same behavior.

I looked into it further, I think it’s because the “blocking” behavior provided by streamlit-js (which this relies on) is actually calling st.stop() and then triggering a rerun when the JS update finishes.

The button state would be reset on that following rerun, so it doesn’t execute again. I suspect a similar problem would happen (maybe worse) if you used local_storage inside a callback.

I’m not sure if there’s a way to fix this in the current runtime model besides working around it in your app code :\

Thanks for the quick reply, Joshua! I think I was able to get this to work by modifying the __setitem__() method to use st_js() instead of st_js_blocking() and adding a unique key.

Not sure if this is best practice, but it appears to work for my use case. Hope it may help someone else.

def __setitem__(self, key: str, value: Any) -> None:
        value = json.dumps(value, ensure_ascii=False)
        _ls_keys[key] = str(uuid.uuid4())
        code = f"""
        console.debug('setting {key} to local storage');
        localStorage.setItem('{KEY_PREFIX + key}', JSON.stringify('{value}'));
        """
        return st_js(code, _ls_keys[key] + "_set")

Nice, that approach seems to mostly work for me too although it occasionally causes the app to “reset” (all widgets lose their state) especially on save. Not sure if that’s some race condition or something else. I updated the gist. Thanks @dkn-vtl !