openms-insight 0.1.6__py3-none-any.whl → 0.1.8__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.
- openms_insight/__init__.py +1 -1
- openms_insight/components/table.py +490 -28
- openms_insight/core/base.py +20 -0
- openms_insight/core/state.py +142 -24
- openms_insight/js-component/dist/assets/index.js +113 -113
- openms_insight/rendering/bridge.py +231 -139
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/METADATA +3 -3
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/RECORD +10 -10
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/WHEEL +0 -0
- {openms_insight-0.1.6.dist-info → openms_insight-0.1.8.dist-info}/licenses/LICENSE +0 -0
openms_insight/core/state.py
CHANGED
|
@@ -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
|
-
"
|
|
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["
|
|
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.
|
|
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.
|
|
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
|
|
180
|
+
Dict with counters, id, and all selections as top-level keys
|
|
147
181
|
"""
|
|
148
182
|
state = {
|
|
149
|
-
"
|
|
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
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
325
|
+
"""Clear all selections and reset counters."""
|
|
209
326
|
self._state["selections"] = {}
|
|
210
|
-
self._state["
|
|
327
|
+
self._state["selection_counter"] = 0
|
|
328
|
+
self._state["pagination_counter"] = 0
|
|
211
329
|
|
|
212
330
|
def __repr__(self) -> str:
|
|
213
331
|
return (
|