openms-insight 0.1.0__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.
@@ -0,0 +1,215 @@
1
+ """State management for cross-component selection synchronization."""
2
+
3
+ from typing import Any, Dict, Optional
4
+ import numpy as np
5
+
6
+ # Module-level default state manager
7
+ _default_state_manager: Optional['StateManager'] = None
8
+
9
+
10
+ def get_default_state_manager() -> 'StateManager':
11
+ """
12
+ Get or create the default shared StateManager.
13
+
14
+ Returns:
15
+ The default StateManager instance
16
+ """
17
+ global _default_state_manager
18
+ if _default_state_manager is None:
19
+ _default_state_manager = StateManager()
20
+ return _default_state_manager
21
+
22
+
23
+ def reset_default_state_manager() -> None:
24
+ """Reset the default state manager (useful for testing)."""
25
+ global _default_state_manager
26
+ _default_state_manager = None
27
+
28
+
29
+ class StateManager:
30
+ """
31
+ Manages selection state across components with conflict resolution.
32
+
33
+ Features:
34
+ - Counter-based conflict resolution to handle concurrent updates
35
+ - Session ID for multi-tab/session safety
36
+ - Streamlit session_state integration
37
+ - Support for arbitrary selection identifiers
38
+
39
+ The StateManager maintains a dict of selections keyed by identifier names.
40
+ When a component updates a selection, all components sharing that identifier
41
+ will see the new value on the next render.
42
+ """
43
+
44
+ def __init__(self, session_key: str = "svc_state"):
45
+ """
46
+ Initialize the StateManager.
47
+
48
+ Args:
49
+ session_key: Key to use in Streamlit session_state for storing
50
+ state. Use different keys for independent component groups.
51
+ """
52
+ self._session_key = session_key
53
+ self._ensure_session_state()
54
+
55
+ def _ensure_session_state(self) -> None:
56
+ """Ensure session state is initialized."""
57
+ import streamlit as st
58
+
59
+ if self._session_key not in st.session_state:
60
+ st.session_state[self._session_key] = {
61
+ 'counter': 0,
62
+ 'id': float(np.random.random()),
63
+ 'selections': {},
64
+ }
65
+
66
+ @property
67
+ def _state(self) -> Dict[str, Any]:
68
+ """Get the internal state dict from session_state."""
69
+ import streamlit as st
70
+ self._ensure_session_state()
71
+ return st.session_state[self._session_key]
72
+
73
+ @property
74
+ def session_id(self) -> float:
75
+ """Get the unique session ID."""
76
+ return self._state['id']
77
+
78
+ @property
79
+ def counter(self) -> int:
80
+ """Get the current state counter."""
81
+ return self._state['counter']
82
+
83
+ def get_selection(self, identifier: str) -> Any:
84
+ """
85
+ Get current selection value for an identifier.
86
+
87
+ Args:
88
+ identifier: The selection identifier name
89
+
90
+ Returns:
91
+ The current selection value, or None if not set
92
+ """
93
+ return self._state['selections'].get(identifier)
94
+
95
+ def set_selection(self, identifier: str, value: Any) -> bool:
96
+ """
97
+ Set selection value for an identifier.
98
+
99
+ Args:
100
+ identifier: The selection identifier name
101
+ value: The value to set
102
+
103
+ Returns:
104
+ True if the value changed, False otherwise
105
+ """
106
+ current = self._state['selections'].get(identifier)
107
+ if current == value:
108
+ return False
109
+
110
+ self._state['selections'][identifier] = value
111
+ self._state['counter'] += 1
112
+ return True
113
+
114
+ def clear_selection(self, identifier: str) -> bool:
115
+ """
116
+ Clear selection for an identifier.
117
+
118
+ Args:
119
+ identifier: The selection identifier name
120
+
121
+ Returns:
122
+ True if a selection was cleared, False if it wasn't set
123
+ """
124
+ if identifier in self._state['selections']:
125
+ del self._state['selections'][identifier]
126
+ self._state['counter'] += 1
127
+ return True
128
+ return False
129
+
130
+ def get_all_selections(self) -> Dict[str, Any]:
131
+ """
132
+ Get all current selections.
133
+
134
+ Returns:
135
+ Dict mapping identifiers to their selected values
136
+ """
137
+ return self._state['selections'].copy()
138
+
139
+ def get_state_for_vue(self) -> Dict[str, Any]:
140
+ """
141
+ Get state dict formatted for sending to Vue component.
142
+
143
+ Returns:
144
+ Dict with counter, id, and all selections as top-level keys
145
+ """
146
+ state = {
147
+ 'counter': self._state['counter'],
148
+ 'id': self._state['id'],
149
+ }
150
+ state.update(self._state['selections'])
151
+ return state
152
+
153
+ def update_from_vue(self, vue_state: Dict[str, Any]) -> bool:
154
+ """
155
+ Update state from Vue component return value.
156
+
157
+ Uses counter-based conflict resolution: only accepts updates from
158
+ Vue if its counter is >= our counter (prevents stale updates).
159
+
160
+ Args:
161
+ vue_state: State dict returned by Vue component
162
+
163
+ Returns:
164
+ True if state was modified, False otherwise
165
+ """
166
+ if vue_state is None:
167
+ return False
168
+
169
+ # Verify same session (prevents cross-tab interference)
170
+ if vue_state.get('id') != self._state['id']:
171
+ return False
172
+
173
+ # Extract metadata
174
+ vue_counter = vue_state.pop('counter', 0)
175
+ vue_state.pop('id', None)
176
+
177
+ # Filter out internal keys (starting with _)
178
+ vue_state = {k: v for k, v in vue_state.items() if not k.startswith('_')}
179
+
180
+ modified = False
181
+
182
+ # Always accept previously undefined keys (but skip None/undefined values)
183
+ for key, value in vue_state.items():
184
+ if key not in self._state['selections']:
185
+ # Only add if value is not None (undefined in Vue = no selection)
186
+ if value is not None:
187
+ self._state['selections'][key] = value
188
+ modified = True
189
+
190
+ # Only accept conflicting updates if Vue has newer state
191
+ if vue_counter >= self._state['counter']:
192
+ for key, value in vue_state.items():
193
+ if key in self._state['selections']:
194
+ if self._state['selections'][key] != value:
195
+ self._state['selections'][key] = value
196
+ modified = True
197
+
198
+ if modified:
199
+ # Set counter to be at least vue_counter + 1 to reject future stale updates
200
+ # from other Vue components that haven't received the latest state yet
201
+ self._state['counter'] = max(self._state['counter'] + 1, vue_counter + 1)
202
+
203
+ return modified
204
+
205
+ def clear(self) -> None:
206
+ """Clear all selections and reset counter."""
207
+ self._state['selections'] = {}
208
+ self._state['counter'] = 0
209
+
210
+ def __repr__(self) -> str:
211
+ return (
212
+ f"StateManager(session_key='{self._session_key}', "
213
+ f"counter={self.counter}, "
214
+ f"selections={self.get_all_selections()})"
215
+ )