reasoning-deployment-service 0.2.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.

Potentially problematic release.


This version of reasoning-deployment-service might be problematic. Click here for more details.

Files changed (29) hide show
  1. examples/programmatic_usage.py +154 -0
  2. reasoning_deployment_service/__init__.py +25 -0
  3. reasoning_deployment_service/cli_editor/__init__.py +5 -0
  4. reasoning_deployment_service/cli_editor/api_client.py +666 -0
  5. reasoning_deployment_service/cli_editor/cli_runner.py +343 -0
  6. reasoning_deployment_service/cli_editor/config.py +82 -0
  7. reasoning_deployment_service/cli_editor/google_deps.py +29 -0
  8. reasoning_deployment_service/cli_editor/reasoning_engine_creator.py +448 -0
  9. reasoning_deployment_service/gui_editor/__init__.py +5 -0
  10. reasoning_deployment_service/gui_editor/main.py +280 -0
  11. reasoning_deployment_service/gui_editor/requirements_minimal.txt +54 -0
  12. reasoning_deployment_service/gui_editor/run_program.sh +55 -0
  13. reasoning_deployment_service/gui_editor/src/__init__.py +1 -0
  14. reasoning_deployment_service/gui_editor/src/core/__init__.py +1 -0
  15. reasoning_deployment_service/gui_editor/src/core/api_client.py +647 -0
  16. reasoning_deployment_service/gui_editor/src/core/config.py +43 -0
  17. reasoning_deployment_service/gui_editor/src/core/google_deps.py +22 -0
  18. reasoning_deployment_service/gui_editor/src/core/reasoning_engine_creator.py +448 -0
  19. reasoning_deployment_service/gui_editor/src/ui/__init__.py +1 -0
  20. reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py +312 -0
  21. reasoning_deployment_service/gui_editor/src/ui/authorization_view.py +280 -0
  22. reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py +354 -0
  23. reasoning_deployment_service/gui_editor/src/ui/reasoning_engines_view.py +204 -0
  24. reasoning_deployment_service/gui_editor/src/ui/ui_components.py +1221 -0
  25. reasoning_deployment_service/reasoning_deployment_service.py +687 -0
  26. reasoning_deployment_service-0.2.8.dist-info/METADATA +177 -0
  27. reasoning_deployment_service-0.2.8.dist-info/RECORD +29 -0
  28. reasoning_deployment_service-0.2.8.dist-info/WHEEL +5 -0
  29. reasoning_deployment_service-0.2.8.dist-info/top_level.txt +2 -0
@@ -0,0 +1,312 @@
1
+ """Agent Space management view."""
2
+ import time
3
+ import tkinter as tk
4
+ from tkinter import ttk, messagebox
5
+ from typing import Callable
6
+
7
+ from ..core.api_client import ApiClient
8
+ from .ui_components import async_operation, StatusButton, AgentDetailsDialog, LoadingDialog
9
+
10
+
11
+ class AgentSpaceView(ttk.Frame):
12
+ """UI for listing and deleting Agent Space agents."""
13
+
14
+ def __init__(self, master, api: ApiClient, log: Callable[[str], None]):
15
+ super().__init__(master)
16
+ self.api = api
17
+ self.log = log
18
+
19
+ # Cache authentication state to avoid repeated API calls
20
+ self._cached_auth_state = None
21
+ self._last_auth_check = 0
22
+ self._auth_cache_duration = 30 # 30 seconds
23
+
24
+ self._setup_ui()
25
+
26
+ def _setup_ui(self):
27
+ # Control buttons
28
+ btns = ttk.Frame(self)
29
+ btns.pack(fill="x", pady=(6, 4))
30
+
31
+ self.refresh_btn = StatusButton(btns, text="Refresh Agents", command=self.refresh)
32
+ self.refresh_btn.pack(side="left", padx=4)
33
+
34
+ self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
35
+ self.delete_btn.pack(side="left", padx=8)
36
+
37
+ self.details_btn = StatusButton(btns, text="More Agent Details", command=self.show_agent_details)
38
+ self.details_btn.pack(side="left", padx=8)
39
+
40
+ self.status = tk.StringVar(value="Ready.")
41
+ ttk.Label(btns, textvariable=self.status).pack(side="right")
42
+
43
+ # Add dropdown for environment selection
44
+ self.env_var = tk.StringVar(value="dev")
45
+ ttk.Label(btns, text="Environment:").pack(side="left", padx=(10, 0))
46
+ ttk.OptionMenu(btns, self.env_var, "dev", "prod").pack(side="left", padx=(0, 10))
47
+
48
+ # Agent list
49
+ wrap = ttk.Frame(self)
50
+ wrap.pack(fill="both", expand=True)
51
+ cols = ("id", "display_name", "authorization_id", "engine_id")
52
+ self.tree = ttk.Treeview(wrap, columns=cols, show="headings", selectmode="extended")
53
+
54
+ for c, t, w in [
55
+ ("id", "Agent ID", 250),
56
+ ("display_name", "Display Name", 250),
57
+ ("authorization_id", "Authorization", 250),
58
+ ("engine_id", "Engine", 250),
59
+ ]:
60
+ self.tree.heading(c, text=t)
61
+ self.tree.column(c, width=w, anchor="w")
62
+
63
+ self.tree.pack(side="left", fill="both", expand=True)
64
+ vsb = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
65
+ self.tree.configure(yscroll=vsb.set)
66
+ vsb.pack(side="right", fill="y")
67
+
68
+ # Event bindings
69
+ self.tree.bind("<<TreeviewSelect>>", self._on_selection_change)
70
+ self.tree.bind("<Button-3>", self._popup)
71
+
72
+ # Context menu
73
+ self.menu = tk.Menu(self, tearoff=0)
74
+ self.menu.add_command(label="Delete", command=self.delete_selected)
75
+
76
+ # Store full agent data for details popup
77
+ self._agents_data = {}
78
+
79
+ # Cache selection state to avoid redundant tree.selection() calls
80
+ self._cached_selection = None
81
+ self._selection_is_dirty = True
82
+
83
+ # Initialize button states without triggering immediate API calls
84
+ self._update_button_states()
85
+
86
+ def _get_cached_auth_state(self) -> bool:
87
+ """Get authentication state with local caching to reduce API calls."""
88
+ now = time.time()
89
+
90
+ # Use cached result if still fresh
91
+ if (self._cached_auth_state is not None and
92
+ (now - self._last_auth_check) < self._auth_cache_duration):
93
+ return self._cached_auth_state
94
+
95
+ # Check authentication and cache result
96
+ self._cached_auth_state = self.api.is_authenticated
97
+ self._last_auth_check = now
98
+ return self._cached_auth_state
99
+
100
+ def _get_selection(self):
101
+ """Get cached selection to avoid redundant tree.selection() calls."""
102
+ if self._selection_is_dirty or self._cached_selection is None:
103
+ self._cached_selection = self.tree.selection()
104
+ self._selection_is_dirty = False
105
+ return self._cached_selection
106
+
107
+ def _update_button_states(self):
108
+ """Update button states based on current conditions - IMMEDIATE, no timers."""
109
+ # Refresh button - always enabled if authenticated (use cached state)
110
+ is_auth = self._get_cached_auth_state()
111
+ self.refresh_btn.set_enabled(
112
+ is_auth,
113
+ "Authentication required" if not is_auth else ""
114
+ )
115
+
116
+ # Get selection once and cache it
117
+ selection = self._get_selection()
118
+ has_selection = bool(selection)
119
+ single_selection = len(selection) == 1
120
+
121
+ # Delete button - enabled only if agents are selected
122
+ self.delete_btn.set_enabled(
123
+ has_selection,
124
+ "Select agents to delete" if not has_selection else ""
125
+ )
126
+
127
+ # Details button - enabled only if a single agent is selected
128
+ self.details_btn.set_enabled(
129
+ single_selection,
130
+ "Select a single agent to view details" if not single_selection else ""
131
+ )
132
+
133
+ def _on_selection_change(self, event=None):
134
+ """Handle tree selection changes - IMMEDIATE update, no debouncing."""
135
+ # Mark selection cache as dirty
136
+ self._selection_is_dirty = True
137
+ # Immediate update - no timers or delays
138
+ self._update_button_states()
139
+
140
+ def _fill_tree_chunked(self, rows, start=0, chunk=200):
141
+ """Fill tree in chunks to prevent UI freezing on large datasets."""
142
+ end = min(start + chunk, len(rows))
143
+ for agent in rows[start:end]:
144
+ iid = self.tree.insert("", "end", values=(
145
+ agent["id"],
146
+ agent["display_name"],
147
+ agent["authorization_id"],
148
+ agent["engine_id"]
149
+ ))
150
+ # Store full agent data for details popup
151
+ self._agents_data[iid] = agent
152
+
153
+ if end < len(rows):
154
+ # Yield to UI thread, then continue with next chunk
155
+ self.after(0, self._fill_tree_chunked, rows, end, chunk)
156
+ else:
157
+ # All done - update final status and button states
158
+ count = len(rows)
159
+ self.status.set(f"Loaded {count} agent{'s' if count != 1 else ''}.")
160
+ self.log(f"βœ… Loaded {count} agent space agent{'s' if count != 1 else ''}. Data keys: {len(self._agents_data)}")
161
+ self._update_button_states()
162
+
163
+ def refresh(self):
164
+ """Refresh the agent list from the API."""
165
+ if not self._get_cached_auth_state():
166
+ self.log("πŸ” Authentication required.")
167
+ return
168
+
169
+ # Show loading dialog
170
+ loading_dialog = LoadingDialog(self.winfo_toplevel(), "Loading agents...")
171
+ self.refresh_btn.set_enabled(False, "Loading...")
172
+ self.status.set("Loading agents...")
173
+
174
+ def callback(result):
175
+ # Close loading dialog
176
+ loading_dialog.close()
177
+ self.refresh_btn.set_enabled(True)
178
+
179
+ if isinstance(result, Exception):
180
+ self.status.set(f"Error: {result}")
181
+ self.log(f"❌ Error loading agents: {result}")
182
+ return
183
+
184
+ # Bulk delete all existing rows (efficient)
185
+ self.tree.delete(*self.tree.get_children())
186
+
187
+ # Clear stored data and invalidate selection cache
188
+ self._agents_data.clear()
189
+ self._selection_is_dirty = True
190
+
191
+ # Fill tree in chunks to prevent UI freezing
192
+ if result:
193
+ self._fill_tree_chunked(result)
194
+ else:
195
+ # No agents
196
+ self.status.set("No agents found.")
197
+ self.log("ℹ️ No agents found.")
198
+ self._update_button_states()
199
+
200
+ async_operation(self.api.list_agent_space_agents, callback=callback, ui_widget=self)
201
+
202
+ def delete_selected(self):
203
+ """Delete selected agents."""
204
+ # Use cached selection to avoid redundant tree.selection() call
205
+ selection = self._get_selection()
206
+ if not selection:
207
+ return
208
+
209
+ count = len(selection)
210
+ if not messagebox.askyesno("Confirm", f"Delete {count} agent{'s' if count != 1 else ''}?"):
211
+ return
212
+
213
+ # Get all selected agents
214
+ agents_to_delete = []
215
+ for item in selection:
216
+ values = self.tree.item(item, "values")
217
+ if len(values) >= 4:
218
+ agent_id = values[0]
219
+ display_name = values[1]
220
+ # Find full_name from the API data
221
+ full_name = f"projects/{self.api.project_id}/locations/global/collections/default_collection/engines/{self.api.engine_name}/assistants/default_assistant/agents/{agent_id}"
222
+ agents_to_delete.append((item, agent_id, display_name, full_name))
223
+
224
+ if not agents_to_delete:
225
+ return
226
+
227
+ self.status.set(f"Deleting {count} agent{'s' if count != 1 else ''}...")
228
+
229
+ def delete_next(index=0):
230
+ if index >= len(agents_to_delete):
231
+ # All done
232
+ self.status.set(f"Deleted {count} agent{'s' if count != 1 else ''}.")
233
+ self.log(f"βœ… Deleted {count} agent{'s' if count != 1 else ''}.")
234
+ # Update button states after deletion
235
+ self._update_button_states()
236
+ return
237
+
238
+ item, agent_id, display_name, full_name = agents_to_delete[index]
239
+
240
+ def callback(result):
241
+ if isinstance(result, Exception):
242
+ self.log(f"❌ Failed to delete {display_name}: {result}")
243
+ else:
244
+ status, message = result
245
+ if status == "deleted":
246
+ self.tree.delete(item)
247
+ # Invalidate selection cache since tree changed
248
+ self._selection_is_dirty = True
249
+ self.log(f"βœ… Deleted agent: {display_name}")
250
+ else:
251
+ self.log(f"⚠️ {display_name}: {message}")
252
+
253
+ # Continue with next deletion
254
+ delete_next(index + 1)
255
+
256
+ async_operation(lambda: self.api.delete_agent_from_space(full_name), callback=callback, ui_widget=self)
257
+
258
+ delete_next()
259
+
260
+ def show_agent_details(self):
261
+ """Show details for the selected agent."""
262
+ # Use cached selection to avoid redundant tree.selection() call
263
+ sel = self._get_selection()
264
+ if len(sel) != 1:
265
+ messagebox.showinfo("No selection", "Select a single agent to view details.")
266
+ return
267
+
268
+ item_id = sel[0]
269
+ if item_id not in self._agents_data:
270
+ messagebox.showerror("Error", "Agent data not found.")
271
+ return
272
+
273
+ agent_data = self._agents_data[item_id]
274
+ try:
275
+ AgentDetailsDialog(self, agent_data)
276
+ except Exception as e:
277
+ print(f"Error opening agent details dialog: {e}")
278
+ messagebox.showerror("Error", f"Failed to open details dialog: {e}")
279
+
280
+ def _popup(self, event):
281
+ """Show context menu."""
282
+ row = self.tree.identify_row(event.y)
283
+ if not row:
284
+ return
285
+
286
+ # Always select the right-clicked row for clarity
287
+ # This ensures context menu operates on the visible selection
288
+ self.tree.selection_set(row)
289
+ self._selection_is_dirty = True
290
+
291
+ try:
292
+ self.menu.tk_popup(event.x_root, event.y_root)
293
+ finally:
294
+ self.menu.grab_release()
295
+
296
+ def update_api(self, api: ApiClient):
297
+ """Update the API client reference."""
298
+ self.api = api
299
+ # Clear cached auth state when API changes
300
+ self._cached_auth_state = None
301
+ self._last_auth_check = 0
302
+ # Update button states immediately
303
+ self._update_button_states()
304
+
305
+ # Add validation for required fields
306
+ def validate_env(self):
307
+ required_fields = ["project_id", "project_number", "location", "agent_space", "engine"]
308
+ missing_fields = [field for field in required_fields if not getattr(self.api, field, None)]
309
+ if missing_fields:
310
+ messagebox.showerror("Validation Error", f"Missing required fields: {', '.join(missing_fields)}")
311
+ return False
312
+ return True
@@ -0,0 +1,280 @@
1
+ """Authorization management view."""
2
+ import time
3
+ import tkinter as tk
4
+ from tkinter import ttk, messagebox
5
+ from typing import Callable
6
+
7
+ from ..core.api_client import ApiClient
8
+ from .ui_components import async_operation, StatusButton, LoadingDialog
9
+
10
+
11
+ class AuthorizationView(ttk.Frame):
12
+ """UI for managing OAuth authorizations."""
13
+
14
+ def __init__(self, master, api: ApiClient, log: Callable[[str], None]):
15
+ super().__init__(master)
16
+ self.api = api
17
+ self.log = log
18
+ self._auth_auto_loaded = False # Track if authorizations have been auto-loaded
19
+
20
+ # Cache authentication state to avoid repeated API calls
21
+ self._cached_auth_state = None
22
+ self._last_auth_check = 0
23
+ self._auth_cache_duration = 30 # 30 seconds
24
+
25
+ self._setup_ui()
26
+
27
+ def _setup_ui(self):
28
+ # Control buttons
29
+ btns = ttk.Frame(self)
30
+ btns.pack(fill="x", pady=(6, 4))
31
+
32
+ self.refresh_btn = StatusButton(btns, text="Refresh Authorizations", command=self.refresh)
33
+ self.refresh_btn.pack(side="left", padx=4)
34
+
35
+ self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
36
+ self.delete_btn.pack(side="left", padx=8)
37
+
38
+ self.status = tk.StringVar(value="Ready.")
39
+ ttk.Label(btns, textvariable=self.status).pack(side="right")
40
+
41
+ # Info label
42
+ info_frame = ttk.Frame(self)
43
+ info_frame.pack(fill="x", padx=4, pady=4)
44
+ ttk.Label(info_frame, text="πŸ’‘ Authorizations enable agents to access Google Drive, Sheets, etc.",
45
+ foreground="gray").pack(anchor="w")
46
+
47
+ # Authorization list
48
+ wrap = ttk.Frame(self)
49
+ wrap.pack(fill="both", expand=True)
50
+ cols = ("id", "name")
51
+ self.tree = ttk.Treeview(wrap, columns=cols, show="headings", selectmode="extended")
52
+
53
+ for c, t, w in [
54
+ ("id", "Authorization ID", 300),
55
+ ("name", "Full Resource Name", 600),
56
+ ]:
57
+ self.tree.heading(c, text=t)
58
+ self.tree.column(c, width=w, anchor="w")
59
+
60
+ self.tree.pack(side="left", fill="both", expand=True)
61
+ vsb = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
62
+ self.tree.configure(yscroll=vsb.set)
63
+ vsb.pack(side="right", fill="y")
64
+
65
+ # Event bindings
66
+ self.tree.bind("<<TreeviewSelect>>", self._on_selection_change)
67
+ self.tree.bind("<Button-3>", self._popup)
68
+
69
+ # Context menu
70
+ self.menu = tk.Menu(self, tearoff=0)
71
+ self.menu.add_command(label="Delete", command=self.delete_selected)
72
+
73
+ # Debouncing for button updates
74
+ self._update_timer = None
75
+
76
+ # Store full authorization data
77
+ self._authorizations_data = {}
78
+
79
+ # Cache selection state to avoid redundant tree.selection() calls
80
+ self._cached_selection = None
81
+ self._selection_is_dirty = True
82
+
83
+ # Initialize button states without immediate API calls
84
+ self._update_button_states()
85
+
86
+ def _get_cached_auth_state(self) -> bool:
87
+ """Get authentication state with local caching to reduce API calls."""
88
+ now = time.time()
89
+
90
+ # Use cached result if still fresh
91
+ if (self._cached_auth_state is not None and
92
+ (now - self._last_auth_check) < self._auth_cache_duration):
93
+ return self._cached_auth_state
94
+
95
+ # Check authentication and cache result
96
+ self._cached_auth_state = self.api.is_authenticated
97
+ self._last_auth_check = now
98
+ return self._cached_auth_state
99
+
100
+ def _get_selection(self):
101
+ """Get cached selection to avoid redundant tree.selection() calls."""
102
+ if self._selection_is_dirty or self._cached_selection is None:
103
+ self._cached_selection = self.tree.selection()
104
+ self._selection_is_dirty = False
105
+ return self._cached_selection
106
+
107
+ def _update_button_states(self):
108
+ """Update button states based on current conditions."""
109
+ # Use cached authentication state to reduce API calls
110
+ is_auth = self._get_cached_auth_state()
111
+ self.refresh_btn.set_enabled(
112
+ is_auth,
113
+ "Authentication required" if not is_auth else ""
114
+ )
115
+
116
+ # Delete button - enabled only if authorizations are selected
117
+ has_selection = bool(self._get_selection())
118
+ self.delete_btn.set_enabled(
119
+ has_selection,
120
+ "Select authorizations to delete" if not has_selection else ""
121
+ )
122
+
123
+ def _on_selection_change(self, event=None):
124
+ """Handle selection changes - IMMEDIATE update, no debouncing."""
125
+ # Mark selection cache as dirty
126
+ self._selection_is_dirty = True
127
+ # Immediate update - no timers or delays
128
+ self._update_button_states()
129
+
130
+ def refresh(self):
131
+ """Refresh the list of authorizations."""
132
+ # Update button states immediately on click
133
+ self._update_button_states()
134
+
135
+ # Check authentication once at the start using cached state
136
+ if not self._get_cached_auth_state():
137
+ self.log("❌ Authentication required")
138
+ return
139
+
140
+ # Show loading dialog
141
+ loading_dialog = LoadingDialog(self.winfo_toplevel(), "Loading authorizations...")
142
+ self.refresh_btn.set_enabled(False, "Loading...")
143
+
144
+ def callback(items):
145
+ # Close loading dialog
146
+ loading_dialog.close()
147
+ self.refresh_btn.set_enabled(True)
148
+
149
+ # Bulk delete all rows at once instead of one-by-one
150
+ self.tree.delete(*self.tree.get_children())
151
+
152
+ # Clear stored data
153
+ self._authorizations_data.clear()
154
+
155
+ if isinstance(items, Exception):
156
+ self.log(f"❌ List error: {items}")
157
+ self.status.set("Error")
158
+ return
159
+
160
+ # Use chunked filling for large datasets to avoid UI blocking
161
+ self._fill_tree_chunked(items)
162
+
163
+ async_operation(self.api.list_authorizations, callback=callback, ui_widget=self)
164
+
165
+ def _fill_tree_chunked(self, rows, start=0, chunk=200):
166
+ """Fill tree in chunks to avoid UI blocking with large datasets."""
167
+ end = min(start + chunk, len(rows))
168
+ for it in rows[start:end]:
169
+ # Insert into tree
170
+ item_id = self.tree.insert("", "end", values=(it["id"], it["name"]))
171
+ # Store full data for future use
172
+ self._authorizations_data[item_id] = it
173
+
174
+ if end < len(rows):
175
+ # Yield to UI thread, then continue with next chunk
176
+ self.after(0, self._fill_tree_chunked, rows, end, chunk)
177
+ else:
178
+ # All done - update status and button states
179
+ self.status.set(f"{len(rows)} authorization(s)")
180
+ self._update_button_states()
181
+
182
+ def delete_selected(self):
183
+ """Delete selected authorizations."""
184
+ # Use cached selection to avoid redundant tree.selection() call
185
+ sel = self._get_selection()
186
+ if not sel:
187
+ messagebox.showinfo("No selection", "Select one or more authorizations to delete.")
188
+ return
189
+
190
+ # Get authorization IDs from stored data
191
+ auth_ids = []
192
+ for item_id in sel:
193
+ if item_id in self._authorizations_data:
194
+ auth_ids.append(self._authorizations_data[item_id]["id"])
195
+
196
+ if not auth_ids:
197
+ messagebox.showerror("Error", "Could not find authorization IDs for selected items.")
198
+ return
199
+
200
+ # Confirm deletion
201
+ count = len(auth_ids)
202
+ msg = f"Delete {count} authorization{'s' if count > 1 else ''}?\n\n"
203
+ msg += "\n".join(f"β€’ {auth_id}" for auth_id in auth_ids[:5])
204
+ if count > 5:
205
+ msg += f"\n... and {count - 5} more"
206
+
207
+ if not messagebox.askyesno("Confirm deletion", msg):
208
+ return
209
+
210
+ self.log(f"πŸ—‘οΈ Deleting {count} authorization{'s' if count > 1 else ''}...")
211
+ self.delete_btn.set_enabled(False, "Deleting...")
212
+
213
+ def callback(results):
214
+ self.delete_btn.set_enabled(True)
215
+
216
+ if isinstance(results, Exception):
217
+ self.log(f"❌ Delete error: {results}")
218
+ return
219
+
220
+ # Batch log messages to reduce UI spam
221
+ ok = [k for k, (s, _) in results.items() if s == "deleted"]
222
+ bad = {k: v for k, v in results.items() if v[0] != "deleted"}
223
+
224
+ if ok:
225
+ ok_display = ", ".join(ok[:10]) + ("…" if len(ok) > 10 else "")
226
+ self.log(f"βœ… Deleted {len(ok)} authorization(s): {ok_display}")
227
+
228
+ # Show first 10 failures with details
229
+ for k, (s, msg) in list(bad.items())[:10]:
230
+ self.log(f"❌ {k}: {msg}")
231
+ if len(bad) > 10:
232
+ self.log(f"…and {len(bad)-10} more failures")
233
+
234
+ self._update_button_states()
235
+ # Auto-refresh to show updated list
236
+ self.refresh()
237
+
238
+ # Delete authorizations sequentially (to avoid API rate limits)
239
+ def delete_multiple():
240
+ results = {}
241
+ for auth_id in auth_ids:
242
+ try:
243
+ results[auth_id] = self.api.delete_authorization(auth_id)
244
+ except Exception as e:
245
+ results[auth_id] = ("failed", str(e))
246
+ return results
247
+
248
+ async_operation(delete_multiple, callback=callback, ui_widget=self)
249
+
250
+ def _popup(self, event):
251
+ """Show context menu."""
252
+ try:
253
+ self.menu.tk_popup(event.x_root, event.y_root)
254
+ finally:
255
+ self.menu.grab_release()
256
+
257
+ def update_api(self, api: ApiClient):
258
+ """Update the API client reference."""
259
+ self.api = api
260
+ # Clear cached auth state when API changes
261
+ self._cached_auth_state = None
262
+ self._last_auth_check = 0
263
+ # Reset auto-load flag
264
+ self._auth_auto_loaded = False
265
+ # Update button states immediately
266
+ self._update_button_states()
267
+
268
+ # Auto-load authorizations if credentials are available and not loaded yet
269
+ if self._get_cached_auth_state():
270
+ self._auth_auto_loaded = True
271
+ self.log("βœ… Auto-loading authorizations...")
272
+ # Use a small delay to ensure UI is ready
273
+ self.after(50, self.refresh)
274
+
275
+ def on_tab_selected(self):
276
+ """Called when this tab is selected - trigger auto-loading if needed."""
277
+ if not self._auth_auto_loaded and self._get_cached_auth_state():
278
+ self._auth_auto_loaded = True
279
+ self.log("βœ… Auto-loading authorizations...")
280
+ self.refresh()