openms-insight 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,15 @@
1
1
  """State management for cross-component selection synchronization."""
2
2
 
3
+ import logging
4
+ import os
3
5
  from typing import Any, Dict, Optional
4
6
 
5
7
  import numpy as np
6
8
 
9
+ # Debug logging for state sync issues
10
+ _DEBUG_STATE_SYNC = os.environ.get("SVC_DEBUG_STATE", "").lower() == "true"
11
+ _logger = logging.getLogger(__name__)
12
+
7
13
  # Module-level default state manager
8
14
  _default_state_manager: Optional["StateManager"] = None
9
15
 
@@ -53,16 +59,28 @@ class StateManager:
53
59
  self._session_key = session_key
54
60
  self._ensure_session_state()
55
61
 
62
+ def _is_pagination_identifier(self, identifier: str) -> bool:
63
+ """Check if identifier is for pagination state (ends with '_page')."""
64
+ return identifier.endswith("_page")
65
+
56
66
  def _ensure_session_state(self) -> None:
57
67
  """Ensure session state is initialized."""
58
68
  import streamlit as st
59
69
 
60
70
  if self._session_key not in st.session_state:
61
71
  st.session_state[self._session_key] = {
62
- "counter": 0,
72
+ "selection_counter": 0,
73
+ "pagination_counter": 0,
63
74
  "id": float(np.random.random()),
64
75
  "selections": {},
65
76
  }
77
+ # Migration: add new counter keys if missing (backwards compat)
78
+ state = st.session_state[self._session_key]
79
+ if "selection_counter" not in state:
80
+ # Migrate from legacy single counter
81
+ legacy_counter = state.get("counter", 0)
82
+ state["selection_counter"] = legacy_counter
83
+ state["pagination_counter"] = legacy_counter
66
84
 
67
85
  @property
68
86
  def _state(self) -> Dict[str, Any]:
@@ -77,10 +95,20 @@ class StateManager:
77
95
  """Get the unique session ID."""
78
96
  return self._state["id"]
79
97
 
98
+ @property
99
+ def selection_counter(self) -> int:
100
+ """Get the current selection counter."""
101
+ return self._state["selection_counter"]
102
+
103
+ @property
104
+ def pagination_counter(self) -> int:
105
+ """Get the current pagination counter."""
106
+ return self._state["pagination_counter"]
107
+
80
108
  @property
81
109
  def counter(self) -> int:
82
- """Get the current state counter."""
83
- return self._state["counter"]
110
+ """Get the current state counter (backwards compatibility)."""
111
+ return max(self._state["selection_counter"], self._state["pagination_counter"])
84
112
 
85
113
  def get_selection(self, identifier: str) -> Any:
86
114
  """
@@ -110,7 +138,10 @@ class StateManager:
110
138
  return False
111
139
 
112
140
  self._state["selections"][identifier] = value
113
- self._state["counter"] += 1
141
+ if self._is_pagination_identifier(identifier):
142
+ self._state["pagination_counter"] += 1
143
+ else:
144
+ self._state["selection_counter"] += 1
114
145
  return True
115
146
 
116
147
  def clear_selection(self, identifier: str) -> bool:
@@ -125,7 +156,10 @@ class StateManager:
125
156
  """
126
157
  if identifier in self._state["selections"]:
127
158
  del self._state["selections"][identifier]
128
- self._state["counter"] += 1
159
+ if self._is_pagination_identifier(identifier):
160
+ self._state["pagination_counter"] += 1
161
+ else:
162
+ self._state["selection_counter"] += 1
129
163
  return True
130
164
  return False
131
165
 
@@ -143,10 +177,15 @@ class StateManager:
143
177
  Get state dict formatted for sending to Vue component.
144
178
 
145
179
  Returns:
146
- Dict with counter, id, and all selections as top-level keys
180
+ Dict with counters, id, and all selections as top-level keys
147
181
  """
148
182
  state = {
149
- "counter": self._state["counter"],
183
+ "selection_counter": self._state["selection_counter"],
184
+ "pagination_counter": self._state["pagination_counter"],
185
+ # Backwards compatibility: include legacy counter as max of both
186
+ "counter": max(
187
+ self._state["selection_counter"], self._state["pagination_counter"]
188
+ ),
150
189
  "id": self._state["id"],
151
190
  }
152
191
  state.update(self._state["selections"])
@@ -156,8 +195,9 @@ class StateManager:
156
195
  """
157
196
  Update state from Vue component return value.
158
197
 
159
- Uses counter-based conflict resolution: only accepts updates from
160
- Vue if its counter is >= our counter (prevents stale updates).
198
+ Uses counter-based conflict resolution with separate counters for
199
+ selection and pagination state. This prevents rapid pagination clicks
200
+ from causing legitimate selection updates to be rejected.
161
201
 
162
202
  Args:
163
203
  vue_state: State dict returned by Vue component
@@ -170,16 +210,51 @@ class StateManager:
170
210
 
171
211
  # Verify same session (prevents cross-tab interference)
172
212
  if vue_state.get("id") != self._state["id"]:
213
+ if _DEBUG_STATE_SYNC:
214
+ _logger.warning(
215
+ f"[StateManager] Session mismatch: vue_id={vue_state.get('id')}, "
216
+ f"python_id={self._state['id']}"
217
+ )
173
218
  return False
174
219
 
175
- # Extract metadata
176
- vue_counter = vue_state.pop("counter", 0)
220
+ # Extract metadata - support both new separate counters and legacy single counter
221
+ vue_selection_counter = vue_state.pop("selection_counter", None)
222
+ vue_pagination_counter = vue_state.pop("pagination_counter", None)
223
+ vue_legacy_counter = vue_state.pop("counter", 0)
177
224
  vue_state.pop("id", None)
178
225
 
226
+ # Backwards compat: if Vue doesn't send separate counters, use legacy
227
+ if vue_selection_counter is None:
228
+ vue_selection_counter = vue_legacy_counter
229
+ if vue_pagination_counter is None:
230
+ vue_pagination_counter = vue_legacy_counter
231
+
232
+ old_selection_counter = self._state["selection_counter"]
233
+ old_pagination_counter = self._state["pagination_counter"]
234
+
235
+ # Debug: log pagination state updates
236
+ if _DEBUG_STATE_SYNC:
237
+ pagination_keys = [
238
+ k
239
+ for k in vue_state.keys()
240
+ if "page" in k.lower() and not k.startswith("_")
241
+ ]
242
+ for pk in pagination_keys:
243
+ old_val = self._state["selections"].get(pk)
244
+ new_val = vue_state.get(pk)
245
+ _logger.warning(
246
+ f"[StateManager] Pagination update: key={pk}, "
247
+ f"old={old_val}, new={new_val}, "
248
+ f"vue_pagination_counter={vue_pagination_counter}, "
249
+ f"python_pagination_counter={old_pagination_counter}"
250
+ )
251
+
179
252
  # Filter out internal keys (starting with _)
180
253
  vue_state = {k: v for k, v in vue_state.items() if not k.startswith("_")}
181
254
 
182
255
  modified = False
256
+ selection_modified = False
257
+ pagination_modified = False
183
258
 
184
259
  # Always accept previously undefined keys (but skip None/undefined values)
185
260
  for key, value in vue_state.items():
@@ -188,26 +263,69 @@ class StateManager:
188
263
  if value is not None:
189
264
  self._state["selections"][key] = value
190
265
  modified = True
191
-
192
- # Only accept conflicting updates if Vue has newer state
193
- if vue_counter >= self._state["counter"]:
194
- for key, value in vue_state.items():
195
- if key in self._state["selections"]:
196
- if self._state["selections"][key] != value:
266
+ if self._is_pagination_identifier(key):
267
+ pagination_modified = True
268
+ else:
269
+ selection_modified = True
270
+ if _DEBUG_STATE_SYNC:
271
+ _logger.warning(
272
+ f"[StateManager] NEW KEY: {key}={value} "
273
+ f"(is_pagination={self._is_pagination_identifier(key)})"
274
+ )
275
+
276
+ # For existing keys, check appropriate counter for conflict resolution
277
+ for key, value in vue_state.items():
278
+ if key in self._state["selections"]:
279
+ old_val = self._state["selections"][key]
280
+ if old_val != value:
281
+ # Use appropriate counter based on key type
282
+ is_pagination = self._is_pagination_identifier(key)
283
+ if is_pagination:
284
+ vue_counter = vue_pagination_counter
285
+ python_counter = old_pagination_counter
286
+ else:
287
+ vue_counter = vue_selection_counter
288
+ python_counter = old_selection_counter
289
+
290
+ # Only accept update if Vue has newer state for this type
291
+ if vue_counter >= python_counter:
197
292
  self._state["selections"][key] = value
198
293
  modified = True
199
-
200
- if modified:
201
- # Set counter to be at least vue_counter + 1 to reject future stale updates
202
- # from other Vue components that haven't received the latest state yet
203
- self._state["counter"] = max(self._state["counter"] + 1, vue_counter + 1)
294
+ if is_pagination:
295
+ pagination_modified = True
296
+ else:
297
+ selection_modified = True
298
+ if _DEBUG_STATE_SYNC:
299
+ _logger.warning(
300
+ f"[StateManager] UPDATE: {key}: {old_val} → {value} "
301
+ f"(vue_counter={vue_counter} >= python_counter={python_counter}, "
302
+ f"is_pagination={is_pagination})"
303
+ )
304
+
305
+ # Update appropriate counter(s) if modified
306
+ if selection_modified:
307
+ self._state["selection_counter"] = max(
308
+ self._state["selection_counter"] + 1, vue_selection_counter + 1
309
+ )
310
+ if pagination_modified:
311
+ self._state["pagination_counter"] = max(
312
+ self._state["pagination_counter"] + 1, vue_pagination_counter + 1
313
+ )
314
+
315
+ if _DEBUG_STATE_SYNC:
316
+ _logger.warning(
317
+ f"[StateManager] modified={modified}, "
318
+ f"selection_counter: {old_selection_counter} → {self._state['selection_counter']}, "
319
+ f"pagination_counter: {old_pagination_counter} → {self._state['pagination_counter']}"
320
+ )
204
321
 
205
322
  return modified
206
323
 
207
324
  def clear(self) -> None:
208
- """Clear all selections and reset counter."""
325
+ """Clear all selections and reset counters."""
209
326
  self._state["selections"] = {}
210
- self._state["counter"] = 0
327
+ self._state["selection_counter"] = 0
328
+ self._state["pagination_counter"] = 0
211
329
 
212
330
  def __repr__(self) -> str:
213
331
  return (