Skip to content

Commit

Permalink
Provide app.storage.client as a location to store volatile data which…
Browse files Browse the repository at this point in the history
… only matters for the current connection (#2820)

* Implemented app.storage.session which enables the user to store data in the current Client instance - which in practice means "per browser tab".

* Replaced Client.state by ObservableDict
Moved context import to top of the file

* Renamed app.storage.session to app.storage.client.

Adjusted documentation of app.storage.client.

* Exchanged quotes
Added app.storage.client clear test

* Added documentation for app.storage.client
Dropped implementation client.current_client for now
Added exceptions for calls to app.storage.client from auto index or w/o client connection.
Added tests for changes

* Removed imports, simplified client availability check

* Updated documentation
Removed client connection state checks and tests

* Removed connection test_clear from

* Removed random import, not required for demo anymore

* Resolved merging conflicts with tab extension

* Merge fix

* Merge fix

* minimal updates to documentation

* code review

* Removed line duplication

* improve clearing of client storage

* fix typo

* add overview table

* renaming

* review documentation

---------

Co-authored-by: Rodja Trappe <[email protected]>
Co-authored-by: Falko Schindler <[email protected]>
  • Loading branch information
3 people authored Apr 9, 2024
1 parent d5889c7 commit 1428e6d
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 4 deletions.
2 changes: 2 additions & 0 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .favicon import get_favicon_url
from .javascript_request import JavaScriptRequest
from .logging import log
from .observables import ObservableDict
from .outbox import Outbox
from .version import __version__

Expand Down Expand Up @@ -73,6 +74,7 @@ def __init__(self, page: page, *, shared: bool = False) -> None:
self._body_html = ''

self.page = page
self.storage = ObservableDict()

self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
Expand Down
19 changes: 19 additions & 0 deletions nicegui/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from . import background_tasks, context, core, json, observables
from .logging import log
from .observables import ObservableDict

request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None)

Expand Down Expand Up @@ -156,6 +157,18 @@ def general(self) -> PersistentDict:
"""General storage shared between all users that is persisted on the server (where NiceGUI is executed)."""
return self._general

@property
def client(self) -> ObservableDict:
"""A volatile storage that is only kept during the current connection to the client.
Like `app.storage.tab` data is unique per browser tab but is even more volatile as it is already discarded
when the connection to the client is lost through a page reload or a navigation.
"""
if self._is_in_auto_index_context():
raise RuntimeError('app.storage.client can only be used with page builder functions '
'(https://nicegui.io/documentation/page)')
return context.get_client().storage

@property
def tab(self) -> observables.ObservableDict:
"""A volatile storage that is only kept during the current tab session."""
Expand Down Expand Up @@ -183,6 +196,12 @@ def clear(self) -> None:
"""Clears all storage."""
self._general.clear()
self._users.clear()
try:
client = context.get_client()
except RuntimeError:
pass # no client, could be a pytest
else:
client.storage.clear()
self._tabs.clear()
for filepath in self.path.glob('storage-*.json'):
filepath.unlink()
Expand Down
37 changes: 37 additions & 0 deletions tests/test_storage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
from pathlib import Path
import pytest

import httpx

Expand Down Expand Up @@ -227,3 +228,39 @@ async def page():
screen.click('clear')
screen.wait(0.5)
assert not tab_storages


def test_client_storage(screen: Screen):
def increment():
app.storage.client['counter'] = app.storage.client['counter'] + 1

@ui.page('/')
def page():
app.storage.client['counter'] = 123
ui.button('Increment').on_click(increment)
ui.label().bind_text(app.storage.client, 'counter')

screen.open('/')
screen.should_contain('123')
screen.click('Increment')
screen.wait_for('124')

screen.switch_to(1)
screen.open('/')
screen.should_contain('123')

screen.switch_to(0)
screen.should_contain('124')


def test_clear_client_storage(screen: Screen):
with pytest.raises(RuntimeError): # no context (auto index)
app.storage.client.clear()

@ui.page('/')
def page():
app.storage.client['counter'] = 123
app.storage.client.clear()
assert app.storage.client == {}

screen.open('/')
49 changes: 46 additions & 3 deletions website/documentation/content/storage_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,20 @@

@doc.demo('Storage', '''
NiceGUI offers a straightforward mechanism for data persistence within your application.
It features four built-in storage types:
It features five built-in storage types:
- `app.storage.tab`:
Stored server-side in memory, this dictionary is unique to each tab session and can hold arbitrary objects.
Data will be lost when restarting the server until <https://github.com/zauberzeug/nicegui/discussions/2841> is implemented.
This storage is only available within [page builder functions](/documentation/page)
and requires an established connection, obtainable via [`await client.connected()`](/documentation/page#wait_for_client_connection).
- `app.storage.client`:
Also stored server-side in memory, this dictionary is unique to each client connection and can hold arbitrary objects.
Data will be discarded when the page is reloaded or the user navigates to another page.
Unlike data stored in `app.storage.tab` which can be persisted on the server even for days,
`app.storage.client` helps caching resource-hungry objects such as a streaming or database connection you need to keep alive
for dynamic site updates but would like to discard as soon as the user leaves the page or closes the browser.
This storage is only available within [page builder functions](/documentation/page).
- `app.storage.user`:
Stored server-side, each dictionary is associated with a unique identifier held in a browser session cookie.
Unique to each user, this storage is accessible across all their browser tabs.
Expand All @@ -35,6 +42,16 @@
The user storage and browser storage are only available within `page builder functions </documentation/page>`_
because they are accessing the underlying `Request` object from FastAPI.
Additionally these two types require the `storage_secret` parameter in`ui.run()` to encrypt the browser session cookie.
| Storage type | `tab` | `client` | `user` | `general` | `browser` |
|-----------------------------|--------|----------|--------|-----------|-----------|
| Location | Server | Server | Server | Server | Browser |
| Across tabs | No | No | Yes | Yes | Yes |
| Across browsers | No | No | No | Yes | No |
| Across page reloads | Yes | No | Yes | Yes | Yes |
| Needs page builder function | Yes | Yes | Yes | No | Yes |
| Needs client connection | Yes | No | No | No | No |
| Write only before response | No | No | No | No | Yes |
''')
def storage_demo():
from nicegui import app
Expand Down Expand Up @@ -99,11 +116,37 @@ def ui_state():
It is also more secure to use such a volatile storage for scenarios like logging into a bank account or accessing a password manager.
''')
def tab_storage():
from nicegui import app
from nicegui import app, Client

# @ui.page('/')
# async def index(client):
# async def index(client: Client):
# await client.connected()
with ui.column(): # HIDE
app.storage.tab['count'] = app.storage.tab.get('count', 0) + 1
ui.label(f'Tab reloaded {app.storage.tab["count"]} times')
ui.button('Reload page', on_click=ui.navigate.reload)


@doc.demo('Short-term memory', '''
The goal of `app.storage.client` is to store data only for the duration of the current page visit.
In difference to data stored in `app.storage.tab`
- which is persisted between page changes and even browser restarts as long as the tab is kept open -
the data in `app.storage.client` will be discarded if the user closes the browser, reloads the page or navigates to another page.
This is beneficial for resource-hungry, intentionally short-lived or sensitive data.
An example is a database connection, which should be closed as soon as the user leaves the page.
Additionally, this storage useful if you want to return a page with default settings every time a user reloads.
Meanwhile, it keeps the data alive during in-page navigation.
This is also helpful when updating elements on the site at intervals, such as a live feed.
''')
def short_term_memory():
from nicegui import app

# @ui.page('/')
# async def index():
with ui.column(): # HIDE
cache = app.storage.client
cache['count'] = 0
ui.label().bind_text_from(cache, 'count', lambda n: f'Updated {n} times')
ui.button('Update content',
on_click=lambda: cache.update(count=cache['count'] + 1))
ui.button('Reload page', on_click=ui.navigate.reload)
4 changes: 3 additions & 1 deletion website/documentation/rendering.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def render_content():
element = ui.restructured_text(part.description.replace(':param ', ':'))
else:
element = ui.markdown(part.description)
element.classes('bold-links arrow-links rst-param-tables')
element.classes('bold-links arrow-links')
if ':param' in part.description:
element.classes('rst-param-tables')
if part.ui:
part.ui()
if part.demo:
Expand Down

0 comments on commit 1428e6d

Please sign in to comment.