Source code for klea_utils.ui.web.nicegui.components.storage
#!/usr/bin/env python3
"""
NiceGUI user-storage helpers for Klea pages.
Covers the persistent per-browser identity (``app.storage.user``) and
defends against the stale-session case where NiceGUI raises
``AssertionError`` because the backing ``storage-user-*.json`` file was
lost while the browser still sends the old session cookie.
File: klea_utils/ui/web/nicegui/components/storage.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import logging
from typing import Any
from nicegui import app, core
from nicegui.storage import request_contextvar
logger = logging.getLogger(__name__)
[docs]
async def ensure_user_storage():
"""Return ``app.storage.user``, recreating it if stale.
When the ``.nicegui/storage-user-*.json`` file is missing but the
browser still sends the old session cookie, ``app.storage.user``
raises ``AssertionError``. We warn and recreate the backing
``FilePersistentDict`` for that ``session_id`` so the page can
continue with a fresh ``user_id`` instead of 500.
"""
try:
return app.storage.user
except AssertionError as e:
request = request_contextvar.get()
session_id = request.session.get("id", "unknown") if request else "unknown"
logger.warning(
f"stale nicegui session {session_id = } missing storage, recreating: {e}"
)
if request is not None:
await core.app.storage._create_user_storage(session_id)
return app.storage.user
[docs]
def user_storage_or_none():
"""Return ``app.storage.user`` or ``None`` if stale (no await)."""
try:
return app.storage.user
except AssertionError as e:
request = request_contextvar.get()
session_id = request.session.get("id", "unknown") if request else "unknown"
logger.warning(f"stale nicegui session {session_id = } at storage access: {e}")
return None
[docs]
def safe_set_user(key: str, value: Any) -> None:
"""Set ``app.storage.user[key]`` if storage is available, else warn."""
store = user_storage_or_none()
if store is not None:
store[key] = value
else:
logger.warning(f"skipping persistent set {key}={value!r} due to stale storage")
[docs]
async def resolve_user_id() -> str:
"""Return the persistent per-browser ``user_id``, creating it if missing.
Must be called at the top of the page builder before any ``await``
so ``app.storage.user`` is still in the request context.
:returns: The stored (or freshly generated) ``user_id`` string.
"""
store = await ensure_user_storage()
if "user_id" not in store:
import uuid
store["user_id"] = str(uuid.uuid4())
logger.debug("NEW user_id=%s", store["user_id"])
else:
logger.debug("EXISTING user_id=%s", store["user_id"])
return store["user_id"]