Skip to content

Commit

Permalink
Added documentation for app.storage.client
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Alyxion committed Apr 7, 2024
1 parent ca99fcf commit 08d73bd
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 24 deletions.
5 changes: 0 additions & 5 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@ def is_auto_index_client(self) -> bool:
"""Return True if this client is the auto-index client."""
return self is self.auto_index_client

@staticmethod
def current_client() -> Optional[Client]:
"""Returns the current client if obtainable from the current context."""
return get_client()

@property
def ip(self) -> Optional[str]:
"""Return the IP address of the client, or None if the client is not connected."""
Expand Down
21 changes: 11 additions & 10 deletions nicegui/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,24 @@ def general(self) -> Dict:

@property
def client(self) -> ObservableDict:
"""Client storage that is persisted on the server (where NiceGUI is executed) on a per client
connection basis.
The data is lost when the client disconnects through reloading the page, closing the tab or
navigating away from the page. It can be used to store data that is only relevant for the current view such
as filter settings on a dashboard or in-page navigation. As the data is not persisted it also allows the
storage of data structures such as database connections, pandas tables, numpy arrays, user specific ML models
or other living objects that are not serializable to JSON.
"""
"""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)')
client = context.get_client()
if not client.has_socket_connection:
raise RuntimeError('app.storage.client can only be used with a client connection; '
'see https://nicegui.io/documentation/page#wait_for_client_connection to await it')
return client.state

def clear(self) -> None:
"""Clears all storage."""
self._general.clear()
self._users.clear()
if get_slot_stack():
if get_slot_stack() and not self._is_in_auto_index_context():
self.client.clear()
for filepath in self.path.glob('storage-*.json'):
filepath.unlink()
Expand Down
44 changes: 36 additions & 8 deletions tests/test_client_state.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,51 @@
from nicegui import ui, app
import pytest

from nicegui import ui, app, context
from nicegui.testing import Screen


def test_session_state(screen: Screen):
app.storage.client['counter'] = 123

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

ui.button('Increment').on_click(increment)
ui.label().bind_text(app.storage.client, 'counter')
@ui.page('/')
async def page():
await context.get_client().connected()
app.storage.client['counter'] = 123
ui.button('Increment').on_click(increment)
ui.label().bind_text(app.storage.client, 'counter')
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_no_connection(screen: Screen):
@ui.page('/')
async def page():
with pytest.raises(RuntimeError): # no connection yet
app.storage.client['counter'] = 123

screen.open('/')


def test_clear(screen: Screen):
app.storage.client['counter'] = 123
app.storage.client.clear()
assert 'counter' not in app.storage.client
with pytest.raises(RuntimeError): # no context (auto index)
app.storage.client.clear()

@ui.page('/')
async def page():
await context.get_client().connected()
app.storage.client['counter'] = 123
app.storage.client.clear()
assert 'counter' not in app.storage.client

screen.open('/')
52 changes: 51 additions & 1 deletion website/documentation/content/storage_documentation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
from collections import Counter
from datetime import datetime

Expand All @@ -8,14 +9,19 @@
counter = Counter() # type: ignore
start = datetime.now().strftime(r'%H:%M, %d %B %Y')


doc.title('Storage')


@doc.demo('Storage', '''
NiceGUI offers a straightforward method for data persistence within your application.
It features three built-in storage types:
- `app.storage.client`:
Stored server-side in memory, this dictionary is unique to each client connection and can hold arbitrary
objects.
Data will be lost when the connection is closed due to a page reload or navigation to another page.
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.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 Down Expand Up @@ -86,3 +92,47 @@ def ui_state():
# .classes('w-full').bind_value(app.storage.user, 'note')
# END OF DEMO
ui.textarea('This note is kept between visits').classes('w-full').bind_value(app.storage.user, 'note')


@doc.demo('Short-term memory', '''
The goal of `app.storage.client` is to store data only for the duration of the current page visit and
only as long as the client is connected.
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 may be beneficial for resource hungry or intentionally very short lived data such as a database
connection which should be closed as soon as the user leaves the page, sensitive data or if you on purpose
want to return a page with the default settings every time the user reloads the page while keeping the
data alive during in-page navigation or when updating elements on the site in intervals such as a live feed.
''')
def tab_storage():
from nicegui import app, context

class DbConnection: # dummy class to simulate a database connection
def __init__(self):
self.connection_id = random.randint(0, 9999)

def status(self) -> str:
return random.choice(['healthy', 'unhealthy'])

def get_db_connection(): # per-client db connection
cs = app.storage.client
if 'connection' in cs:
return cs['connection']
cs['connection'] = DbConnection()
return cs['connection']

# @ui.page('/')
# async def index(client):
# await client.connected()
with ui.row(): # HIDE
status = ui.markdown('DB status')
def update_status():
db_con = get_db_connection()
status.set_content('**Database connection ID**: '
f'{db_con.connection_id}\n\n'
f'**Status**: {db_con.status()}')
with ui.row():
ui.button('Refresh', on_click=update_status)
ui.button("Reload page", on_click=lambda: ui.navigate.reload())
update_status()

0 comments on commit 08d73bd

Please sign in to comment.