From db6b065ff4d6f94365d900d89d262c56fd9ac464 Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Thu, 4 Apr 2024 10:12:55 +0200 Subject: [PATCH 01/20] Implemented app.storage.session which enables the user to store data in the current Client instance - which in practice means "per browser tab". --- nicegui/client.py | 7 +++++++ nicegui/storage.py | 12 ++++++++++++ tests/test_session_state.py | 17 +++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/test_session_state.py diff --git a/nicegui/client.py b/nicegui/client.py index 88fd0a2bc..883b8ac73 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -73,6 +73,7 @@ def __init__(self, page: page, *, shared: bool = False) -> None: self._body_html = '' self.page = page + self.state = {} self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = [] self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = [] @@ -84,6 +85,12 @@ 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.""" + from .context import get_client + return get_client() + @property def ip(self) -> Optional[str]: """Return the IP address of the client, or None if the client is not connected.""" diff --git a/nicegui/storage.py b/nicegui/storage.py index 657f8650b..5929c1a2c 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -13,6 +13,7 @@ from starlette.responses import Response from . import background_tasks, context, core, json, observables +from .context import get_slot_stack from .logging import log request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None) @@ -149,10 +150,21 @@ def general(self) -> Dict: """General storage shared between all users that is persisted on the server (where NiceGUI is executed).""" return self._general + @property + def session(self) -> Dict: + """Volatile client storage that is persisted on the server (where NiceGUI is + executed) on a per client/per connection basis. + Note that this kind of storage can only be used in single page applications + where the client connection is preserved between page changes.""" + client = context.get_client() + return client.state + def clear(self) -> None: """Clears all storage.""" self._general.clear() self._users.clear() + if get_slot_stack(): + self.session.clear() for filepath in self.path.glob('storage-*.json'): filepath.unlink() diff --git a/tests/test_session_state.py b/tests/test_session_state.py new file mode 100644 index 000000000..d470f603a --- /dev/null +++ b/tests/test_session_state.py @@ -0,0 +1,17 @@ +from nicegui import ui, app +from nicegui.testing import Screen + + +def test_session_state(screen: Screen): + app.storage.session["counter"] = 123 + + def increment(): + app.storage.session["counter"] = app.storage.session["counter"] + 1 + + ui.button("Increment").on_click(increment) + ui.label().bind_text(app.storage.session, "counter") + + screen.open('/') + screen.should_contain('123') + screen.click('Increment') + screen.wait_for('124') From 3fedd36799f5cbbb5afb2606ba20eeec39dd11a7 Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Fri, 5 Apr 2024 09:46:03 +0200 Subject: [PATCH 02/20] Replaced Client.state by ObservableDict Moved context import to top of the file --- nicegui/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index 883b8ac73..98d1dcc96 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -15,10 +15,12 @@ from . import background_tasks, binding, core, helpers, json from .awaitable_response import AwaitableResponse +from .context import get_client from .dependencies import generate_resources from .element import Element from .favicon import get_favicon_url from .logging import log +from .observables import ObservableDict from .outbox import Outbox from .version import __version__ @@ -73,7 +75,7 @@ def __init__(self, page: page, *, shared: bool = False) -> None: self._body_html = '' self.page = page - self.state = {} + self.state = ObservableDict() self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = [] self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = [] @@ -88,7 +90,6 @@ def is_auto_index_client(self) -> bool: @staticmethod def current_client() -> Optional[Client]: """Returns the current client if obtainable from the current context.""" - from .context import get_client return get_client() @property From ea8dad54b5a404894afe984bcda412aa02fbb6ac Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Sat, 6 Apr 2024 08:41:17 +0200 Subject: [PATCH 03/20] Renamed app.storage.session to app.storage.client. Adjusted documentation of app.storage.client. --- nicegui/storage.py | 18 ++++++++++++------ ...t_session_state.py => test_client_state.py} | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) rename tests/{test_session_state.py => test_client_state.py} (62%) diff --git a/nicegui/storage.py b/nicegui/storage.py index 5929c1a2c..67ae594f2 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -15,6 +15,7 @@ from . import background_tasks, context, core, json, observables from .context import get_slot_stack from .logging import log +from .observables import ObservableDict request_contextvar: contextvars.ContextVar[Optional[Request]] = contextvars.ContextVar('request_var', default=None) @@ -151,11 +152,16 @@ def general(self) -> Dict: return self._general @property - def session(self) -> Dict: - """Volatile client storage that is persisted on the server (where NiceGUI is - executed) on a per client/per connection basis. - Note that this kind of storage can only be used in single page applications - where the client connection is preserved between page changes.""" + 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. + """ client = context.get_client() return client.state @@ -164,7 +170,7 @@ def clear(self) -> None: self._general.clear() self._users.clear() if get_slot_stack(): - self.session.clear() + self.client.clear() for filepath in self.path.glob('storage-*.json'): filepath.unlink() diff --git a/tests/test_session_state.py b/tests/test_client_state.py similarity index 62% rename from tests/test_session_state.py rename to tests/test_client_state.py index d470f603a..61eee7ffc 100644 --- a/tests/test_session_state.py +++ b/tests/test_client_state.py @@ -3,13 +3,13 @@ def test_session_state(screen: Screen): - app.storage.session["counter"] = 123 + app.storage.client["counter"] = 123 def increment(): - app.storage.session["counter"] = app.storage.session["counter"] + 1 + app.storage.client["counter"] = app.storage.client["counter"] + 1 ui.button("Increment").on_click(increment) - ui.label().bind_text(app.storage.session, "counter") + ui.label().bind_text(app.storage.client, "counter") screen.open('/') screen.should_contain('123') From 9dd4227ab630ea647cc70b827c5b7071ea8ddb0d Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Sat, 6 Apr 2024 14:06:01 +0200 Subject: [PATCH 04/20] Exchanged quotes Added app.storage.client clear test --- tests/test_client_state.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_client_state.py b/tests/test_client_state.py index 61eee7ffc..77e53cb42 100644 --- a/tests/test_client_state.py +++ b/tests/test_client_state.py @@ -3,15 +3,21 @@ def test_session_state(screen: Screen): - app.storage.client["counter"] = 123 + app.storage.client['counter'] = 123 def increment(): - app.storage.client["counter"] = app.storage.client["counter"] + 1 + app.storage.client['counter'] = app.storage.client['counter'] + 1 - 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') + + +def test_clear(screen: Screen): + app.storage.client['counter'] = 123 + app.storage.client.clear() + assert 'counter' not in app.storage.client From 08d73bdabd59b260100fb378619204318ea9fa32 Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Sun, 7 Apr 2024 21:32:16 +0200 Subject: [PATCH 05/20] 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 --- nicegui/client.py | 5 -- nicegui/storage.py | 21 ++++---- tests/test_client_state.py | 44 +++++++++++++--- .../content/storage_documentation.py | 52 ++++++++++++++++++- 4 files changed, 98 insertions(+), 24 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index dbe153d7d..00b3610e7 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -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.""" diff --git a/nicegui/storage.py b/nicegui/storage.py index 67ae594f2..8241c6f33 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -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() diff --git a/tests/test_client_state.py b/tests/test_client_state.py index 77e53cb42..3b04e56b9 100644 --- a/tests/test_client_state.py +++ b/tests/test_client_state.py @@ -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('/') diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 792776910..4d2a2833f 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -1,3 +1,4 @@ +import random from collections import Counter from datetime import datetime @@ -8,7 +9,6 @@ counter = Counter() # type: ignore start = datetime.now().strftime(r'%H:%M, %d %B %Y') - doc.title('Storage') @@ -16,6 +16,12 @@ 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. @@ -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() From 06d6393f5b6a40debcc0d876a9c7d300cb14f33b Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Sun, 7 Apr 2024 21:40:36 +0200 Subject: [PATCH 06/20] Removed imports, simplified client availability check --- nicegui/client.py | 1 - nicegui/storage.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index 00b3610e7..c45c1528b 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -15,7 +15,6 @@ from . import background_tasks, binding, core, helpers, json from .awaitable_response import AwaitableResponse -from .context import get_client from .dependencies import generate_resources from .element import Element from .favicon import get_favicon_url diff --git a/nicegui/storage.py b/nicegui/storage.py index 8241c6f33..e92ac3d45 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -170,7 +170,7 @@ def clear(self) -> None: """Clears all storage.""" self._general.clear() self._users.clear() - if get_slot_stack() and not self._is_in_auto_index_context(): + if not self._is_in_auto_index_context() and get_slot_stack(): self.client.clear() for filepath in self.path.glob('storage-*.json'): filepath.unlink() From 040ebb814c41fff56cd6392d0a566872b8404aa8 Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 10:33:07 +0200 Subject: [PATCH 07/20] Updated documentation Removed client connection state checks and tests --- nicegui/storage.py | 3 -- tests/test_client_state.py | 11 ---- .../content/storage_documentation.py | 52 ++++++------------- 3 files changed, 17 insertions(+), 49 deletions(-) diff --git a/nicegui/storage.py b/nicegui/storage.py index e92ac3d45..40b2e0a5a 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -161,9 +161,6 @@ def client(self) -> ObservableDict: 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: diff --git a/tests/test_client_state.py b/tests/test_client_state.py index 3b04e56b9..7d479324e 100644 --- a/tests/test_client_state.py +++ b/tests/test_client_state.py @@ -10,7 +10,6 @@ def increment(): @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') @@ -27,16 +26,6 @@ async def page(): 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): with pytest.raises(RuntimeError): # no context (auto index) app.storage.client.clear() diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 4d2a2833f..0a9967ce9 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -14,14 +14,15 @@ @doc.demo('Storage', ''' NiceGUI offers a straightforward method for data persistence within your application. - It features three built-in storage types: + It features four 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). + objects. Data will be lost 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-intensive objects such as a streaming or database connection you + need to keep alive for dynamic site updates but would like to close 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. @@ -95,8 +96,7 @@ def ui_state(): @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. + 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. @@ -105,34 +105,16 @@ def ui_state(): 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'] +def short_term_memory(): + from nicegui import app # @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() + with ui.column(): # HIDE + app.storage.client['counter'] = 0 + ui.label().bind_text_from(app.storage.client, 'counter', + backward=lambda n: f'Updates: {n}') + ui.button('Update content', + on_click=lambda: app.storage.client.update( + {"counter": app.storage.client["counter"] + 1})) + ui.button("Reload page", on_click=lambda: ui.navigate.reload()) From f02d9e9769598314a056b3514c93f25d31e13c5f Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 10:37:47 +0200 Subject: [PATCH 08/20] Removed connection test_clear from --- tests/test_client_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client_state.py b/tests/test_client_state.py index 7d479324e..34dffd0b9 100644 --- a/tests/test_client_state.py +++ b/tests/test_client_state.py @@ -26,13 +26,13 @@ async def page(): screen.switch_to(0) screen.should_contain('124') + def test_clear(screen: Screen): 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 From c57a93fb2bc21e3c9e5f813d94e50bb49867fcb7 Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 10:38:46 +0200 Subject: [PATCH 09/20] Removed random import, not required for demo anymore --- website/documentation/content/storage_documentation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 0a9967ce9..c503890f6 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -1,4 +1,3 @@ -import random from collections import Counter from datetime import datetime From ac4ecb19a39d135f577151ca985ff22cd05f674a Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 13:18:39 +0200 Subject: [PATCH 10/20] Resolved merging conflicts with tab extension --- website/documentation/content/storage_documentation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 1bee4cc5c..30e7cfadd 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -23,10 +23,10 @@ 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 lost when the page is reloaded or the user navigates to another page. + 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-intensive objects such as a streaming or database connection you - need to keep alive for dynamic site updates but would like to close as soon as the user leaves the page or + `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. @@ -134,7 +134,7 @@ def short_term_memory(): with ui.column(): # HIDE app.storage.client['counter'] = 0 ui.label().bind_text_from(app.storage.client, 'counter', - backward=lambda n: f'Updates: {n}') + backward=lambda n: f'Content updated {n} times') ui.button('Update content', on_click=lambda: app.storage.client.update( {"counter": app.storage.client["counter"] + 1})) From edca3130309e43d36748673878d7eb9ba631e81b Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 13:20:42 +0200 Subject: [PATCH 11/20] Merge fix --- nicegui/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nicegui/storage.py b/nicegui/storage.py index f1c8f5cbf..e13bc8010 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -154,7 +154,7 @@ def _is_in_auto_index_context() -> bool: return False # no client @property - def general(self) -> Dict: + def general(self) -> PersistentDict: """General storage shared between all users that is persisted on the server (where NiceGUI is executed).""" return self._general From 6e85268b8d2f8fcaa12692e89f15a4f2cfe91107 Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 13:22:36 +0200 Subject: [PATCH 12/20] Merge fix --- website/documentation/content/storage_documentation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 30e7cfadd..8bfa796cd 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -24,7 +24,7 @@ - `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, + 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). From bad5cec443fe9cf4979d8e7250c0288ecc5465d0 Mon Sep 17 00:00:00 2001 From: Rodja Trappe Date: Mon, 8 Apr 2024 17:03:54 +0200 Subject: [PATCH 13/20] minimal updates to documentation --- .../content/storage_documentation.py | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 8bfa796cd..968e41539 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -22,12 +22,12 @@ 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. + 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.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. @@ -114,17 +114,18 @@ def tab_storage(): 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=lambda: 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 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. + 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 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 short_term_memory(): from nicegui import app @@ -132,10 +133,10 @@ def short_term_memory(): # @ui.page('/') # async def index(client): with ui.column(): # HIDE - app.storage.client['counter'] = 0 - ui.label().bind_text_from(app.storage.client, 'counter', + cache = app.storage.client + cache['counter'] = 0 + ui.label().bind_text_from(cache, 'counter', backward=lambda n: f'Content updated {n} times') ui.button('Update content', - on_click=lambda: app.storage.client.update( - {"counter": app.storage.client["counter"] + 1})) + on_click=lambda: cache.update({"counter": cache["counter"] + 1})) ui.button("Reload page", on_click=lambda: ui.navigate.reload()) From c01d0d14d4859afc3dfe6065ef7d39aa6b2001cc Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Mon, 8 Apr 2024 18:24:35 +0200 Subject: [PATCH 14/20] code review --- nicegui/storage.py | 9 ++++----- tests/test_client_state.py | 40 -------------------------------------- tests/test_storage.py | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 45 deletions(-) delete mode 100644 tests/test_client_state.py diff --git a/nicegui/storage.py b/nicegui/storage.py index e13bc8010..4c8d02f53 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -16,7 +16,6 @@ from starlette.responses import Response from . import background_tasks, context, core, json, observables -from .context import get_slot_stack from .logging import log from .observables import ObservableDict @@ -163,12 +162,12 @@ 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.""" + 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() - return client.state + return context.get_client().state @property def tab(self) -> observables.ObservableDict: @@ -197,7 +196,7 @@ def clear(self) -> None: """Clears all storage.""" self._general.clear() self._users.clear() - if not self._is_in_auto_index_context() and get_slot_stack(): + if not self._is_in_auto_index_context() and context.get_slot_stack(): self.client.clear() self._tabs.clear() for filepath in self.path.glob('storage-*.json'): diff --git a/tests/test_client_state.py b/tests/test_client_state.py deleted file mode 100644 index 34dffd0b9..000000000 --- a/tests/test_client_state.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest - -from nicegui import ui, app, context -from nicegui.testing import Screen - - -def test_session_state(screen: Screen): - def increment(): - app.storage.client['counter'] = app.storage.client['counter'] + 1 - - @ui.page('/') - async def page(): - 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_clear(screen: Screen): - with pytest.raises(RuntimeError): # no context (auto index) - app.storage.client.clear() - - @ui.page('/') - async def page(): - app.storage.client['counter'] = 123 - app.storage.client.clear() - assert 'counter' not in app.storage.client - - screen.open('/') diff --git a/tests/test_storage.py b/tests/test_storage.py index 8db90f2a7..b179da7cc 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,5 +1,6 @@ import asyncio from pathlib import Path +import pytest import httpx @@ -227,3 +228,41 @@ 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') + 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('/') From 26ba1915dd0f1223db2ef2f062546b173955654c Mon Sep 17 00:00:00 2001 From: Michael Ikemann Date: Mon, 8 Apr 2024 19:51:22 +0200 Subject: [PATCH 15/20] Removed line duplication --- tests/test_storage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_storage.py b/tests/test_storage.py index b179da7cc..a9d3ecadb 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -239,8 +239,6 @@ def page(): 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') From 9ade1f963acea42b5b12ada53915410f5936d675 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 9 Apr 2024 10:11:49 +0200 Subject: [PATCH 16/20] improve clearing of client storage --- nicegui/storage.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nicegui/storage.py b/nicegui/storage.py index 4c8d02f53..580f6b540 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -196,8 +196,12 @@ def clear(self) -> None: """Clears all storage.""" self._general.clear() self._users.clear() - if not self._is_in_auto_index_context() and context.get_slot_stack(): - self.client.clear() + try: + client = context.get_client() + except RuntimeError: + pass # no client, could a pytest + else: + client.state.clear() self._tabs.clear() for filepath in self.path.glob('storage-*.json'): filepath.unlink() From 6563825ab8fb8d9ffd75979b005cfa65f7b6e05c Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 9 Apr 2024 10:15:58 +0200 Subject: [PATCH 17/20] fix typo --- nicegui/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nicegui/storage.py b/nicegui/storage.py index 580f6b540..e6af8617e 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -199,7 +199,7 @@ def clear(self) -> None: try: client = context.get_client() except RuntimeError: - pass # no client, could a pytest + pass # no client, could be a pytest else: client.state.clear() self._tabs.clear() From ff58c036435dfd0befccbb4e86e4cfff9ad06493 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 9 Apr 2024 11:18:16 +0200 Subject: [PATCH 18/20] add overview table --- website/documentation/content/storage_documentation.py | 10 ++++++++++ website/documentation/rendering.py | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 968e41539..3f06b6eec 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -42,6 +42,16 @@ The user storage and browser storage are only available within `page builder functions `_ 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 diff --git a/website/documentation/rendering.py b/website/documentation/rendering.py index c0c71d22d..cba64a79b 100644 --- a/website/documentation/rendering.py +++ b/website/documentation/rendering.py @@ -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: From 7f856b4dedb3fb15fc4a16873b88dc1860276954 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 9 Apr 2024 17:54:21 +0200 Subject: [PATCH 19/20] renaming --- nicegui/client.py | 2 +- nicegui/storage.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index c7e31f3c3..06c6bf686 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -74,7 +74,7 @@ def __init__(self, page: page, *, shared: bool = False) -> None: self._body_html = '' self.page = page - self.state = ObservableDict() + self.storage = ObservableDict() self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = [] self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = [] diff --git a/nicegui/storage.py b/nicegui/storage.py index e6af8617e..1cbdecfe8 100644 --- a/nicegui/storage.py +++ b/nicegui/storage.py @@ -167,7 +167,7 @@ def client(self) -> ObservableDict: 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().state + return context.get_client().storage @property def tab(self) -> observables.ObservableDict: @@ -201,7 +201,7 @@ def clear(self) -> None: except RuntimeError: pass # no client, could be a pytest else: - client.state.clear() + client.storage.clear() self._tabs.clear() for filepath in self.path.glob('storage-*.json'): filepath.unlink() From 0f72368d18d9d94f0c44a555faaff8d12462aa99 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 9 Apr 2024 18:05:17 +0200 Subject: [PATCH 20/20] review documentation --- .../content/storage_documentation.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py index 3f06b6eec..2a0441a94 100644 --- a/website/documentation/content/storage_documentation.py +++ b/website/documentation/content/storage_documentation.py @@ -116,37 +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=lambda: ui.navigate.reload()) + 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 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. - ''') + 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(client): + # async def index(): with ui.column(): # HIDE cache = app.storage.client - cache['counter'] = 0 - ui.label().bind_text_from(cache, 'counter', - backward=lambda n: f'Content updated {n} times') + 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({"counter": cache["counter"] + 1})) - ui.button("Reload page", on_click=lambda: ui.navigate.reload()) + on_click=lambda: cache.update(count=cache['count'] + 1)) + ui.button('Reload page', on_click=ui.navigate.reload)