RandomCatLover Claude Sonnet 5 commited on
Commit
5be8e7e
·
1 Parent(s): 2f83d3e

Fix CachedWidgetWarning: don't cache the CookieManager component

Browse files

st.cache_resource froze the component after its first render, so
cookie reads/writes never reflected later changes. Fixed by
constructing exactly one CookieManager per script run in app.py and
threading it into cookie_token_store's functions as a parameter,
rather than each function lazily constructing (and risking a second,
same-keyed) instance internally -- calling
stx.CookieManager(key=...) twice in one run raises DuplicateWidgetID.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +12 -9
  2. src/cookie_token_store.py +18 -11
app.py CHANGED
@@ -41,7 +41,7 @@ def _access_token_valid() -> bool:
41
  )
42
 
43
 
44
- def _handle_drive_oauth_callback() -> None:
45
  """Completes the one-time "Connect Google Drive" exchange, if we're mid-flow."""
46
  if not st.user.is_logged_in:
47
  return
@@ -64,14 +64,14 @@ def _handle_drive_oauth_callback() -> None:
64
  return
65
 
66
  refresh_token = tokens.get("refresh_token")
67
- if refresh_token and "cookies" in st.secrets:
68
- cookie_token_store.save_refresh_token(st.user.email, refresh_token)
69
  _store_access_token(tokens)
70
  st.query_params.clear()
71
  st.rerun()
72
 
73
 
74
- def _ensure_drive_access() -> None:
75
  """Populates session_state's Drive access token, silently refreshing from a
76
  stored refresh_token when possible. Shows a one-time "Connect Google Drive"
77
  link only when neither a live token nor a stored refresh_token exists.
@@ -79,8 +79,8 @@ def _ensure_drive_access() -> None:
79
  if not st.user.is_logged_in or _access_token_valid():
80
  return
81
 
82
- if "cookies" in st.secrets:
83
- refresh_token = cookie_token_store.get_refresh_token(st.user.email)
84
  if refresh_token:
85
  try:
86
  tokens = google_oauth.refresh_access_token(
@@ -91,7 +91,7 @@ def _ensure_drive_access() -> None:
91
  _store_access_token(tokens)
92
  return
93
  except Exception:
94
- cookie_token_store.delete_refresh_token(st.user.email)
95
 
96
  state = uuid.uuid4().hex
97
  st.session_state["drive_oauth_state"] = state
@@ -101,8 +101,11 @@ def _ensure_drive_access() -> None:
101
  st.link_button("Connect Google Drive", authorize_url)
102
 
103
 
 
 
 
104
  if "auth" in st.secrets:
105
- _handle_drive_oauth_callback()
106
 
107
  with st.sidebar:
108
  if "auth" not in st.secrets:
@@ -112,7 +115,7 @@ with st.sidebar:
112
  else:
113
  st.caption(f"Logged in as {st.user.email}")
114
  st.button("Log out", on_click=st.logout)
115
- _ensure_drive_access()
116
 
117
  home_page = st.Page("pages/home.py", title="Home", url_path="home", default=True)
118
  skull_king_page = st.Page("pages/skull_king.py", title="Skull King", url_path="skull_king")
 
41
  )
42
 
43
 
44
+ def _handle_drive_oauth_callback(cookie_manager) -> None:
45
  """Completes the one-time "Connect Google Drive" exchange, if we're mid-flow."""
46
  if not st.user.is_logged_in:
47
  return
 
64
  return
65
 
66
  refresh_token = tokens.get("refresh_token")
67
+ if refresh_token and cookie_manager is not None:
68
+ cookie_token_store.save_refresh_token(cookie_manager, st.user.email, refresh_token)
69
  _store_access_token(tokens)
70
  st.query_params.clear()
71
  st.rerun()
72
 
73
 
74
+ def _ensure_drive_access(cookie_manager) -> None:
75
  """Populates session_state's Drive access token, silently refreshing from a
76
  stored refresh_token when possible. Shows a one-time "Connect Google Drive"
77
  link only when neither a live token nor a stored refresh_token exists.
 
79
  if not st.user.is_logged_in or _access_token_valid():
80
  return
81
 
82
+ if cookie_manager is not None:
83
+ refresh_token = cookie_token_store.get_refresh_token(cookie_manager, st.user.email)
84
  if refresh_token:
85
  try:
86
  tokens = google_oauth.refresh_access_token(
 
91
  _store_access_token(tokens)
92
  return
93
  except Exception:
94
+ cookie_token_store.delete_refresh_token(cookie_manager, st.user.email)
95
 
96
  state = uuid.uuid4().hex
97
  st.session_state["drive_oauth_state"] = state
 
101
  st.link_button("Connect Google Drive", authorize_url)
102
 
103
 
104
+ # Constructed at most once per script run -- see get_cookie_manager's docstring.
105
+ _cookie_manager = cookie_token_store.get_cookie_manager() if "cookies" in st.secrets else None
106
+
107
  if "auth" in st.secrets:
108
+ _handle_drive_oauth_callback(_cookie_manager)
109
 
110
  with st.sidebar:
111
  if "auth" not in st.secrets:
 
115
  else:
116
  st.caption(f"Logged in as {st.user.email}")
117
  st.button("Log out", on_click=st.logout)
118
+ _ensure_drive_access(_cookie_manager)
119
 
120
  home_page = st.Page("pages/home.py", title="Home", url_path="home", default=True)
121
  skull_king_page = st.Page("pages/skull_king.py", title="Skull King", url_path="skull_king")
src/cookie_token_store.py CHANGED
@@ -8,8 +8,16 @@ from cryptography.fernet import Fernet
8
  _COOKIE_MAX_AGE_DAYS = 400 # ~ the longest lifetime browsers allow anyway (Chrome caps at 400 days)
9
 
10
 
11
- @st.cache_resource
12
- def _cookie_manager() -> stx.CookieManager:
 
 
 
 
 
 
 
 
13
  return stx.CookieManager(key="bgt_cookie_manager")
14
 
15
 
@@ -22,16 +30,15 @@ def _cookie_name(email: str) -> str:
22
  return "bgt_rt_" + hashlib.sha256(email.encode()).hexdigest()[:16]
23
 
24
 
25
- def save_refresh_token(email: str, refresh_token: str) -> None:
26
  encrypted = _fernet().encrypt(refresh_token.encode()).decode()
27
  expires_at = datetime.now(timezone.utc) + timedelta(days=_COOKIE_MAX_AGE_DAYS)
28
- _cookie_manager().set(
29
- _cookie_name(email), encrypted, expires_at=expires_at, key=f"set_{_cookie_name(email)}"
30
- )
31
 
32
 
33
- def get_refresh_token(email: str) -> str | None:
34
- encrypted = _cookie_manager().get(_cookie_name(email))
35
  if not encrypted:
36
  return None
37
  try:
@@ -40,7 +47,7 @@ def get_refresh_token(email: str) -> str | None:
40
  return None
41
 
42
 
43
- def delete_refresh_token(email: str) -> None:
44
  name = _cookie_name(email)
45
- if _cookie_manager().get(name) is not None:
46
- _cookie_manager().delete(name, key=f"delete_{name}")
 
8
  _COOKIE_MAX_AGE_DAYS = 400 # ~ the longest lifetime browsers allow anyway (Chrome caps at 400 days)
9
 
10
 
11
+ def get_cookie_manager() -> stx.CookieManager:
12
+ """Constructs the cookie-sync component.
13
+
14
+ Must be called exactly once per script run (not cached, not called more
15
+ than once) -- the underlying component re-renders every rerun to report
16
+ the browser's current cookies back to Python, so caching it (via
17
+ st.cache_resource or otherwise) freezes it stale, and constructing a
18
+ second instance with the same key in the same run raises
19
+ DuplicateWidgetID.
20
+ """
21
  return stx.CookieManager(key="bgt_cookie_manager")
22
 
23
 
 
30
  return "bgt_rt_" + hashlib.sha256(email.encode()).hexdigest()[:16]
31
 
32
 
33
+ def save_refresh_token(cookie_manager: stx.CookieManager, email: str, refresh_token: str) -> None:
34
  encrypted = _fernet().encrypt(refresh_token.encode()).decode()
35
  expires_at = datetime.now(timezone.utc) + timedelta(days=_COOKIE_MAX_AGE_DAYS)
36
+ name = _cookie_name(email)
37
+ cookie_manager.set(name, encrypted, expires_at=expires_at, key=f"set_{name}")
 
38
 
39
 
40
+ def get_refresh_token(cookie_manager: stx.CookieManager, email: str) -> str | None:
41
+ encrypted = cookie_manager.get(_cookie_name(email))
42
  if not encrypted:
43
  return None
44
  try:
 
47
  return None
48
 
49
 
50
+ def delete_refresh_token(cookie_manager: stx.CookieManager, email: str) -> None:
51
  name = _cookie_name(email)
52
+ if cookie_manager.get(name) is not None:
53
+ cookie_manager.delete(name, key=f"delete_{name}")