glaip-sdk 0.1.0__py3-none-any.whl → 0.1.2__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.
- glaip_sdk/_version.py +1 -3
- glaip_sdk/branding.py +2 -6
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +11 -30
- glaip_sdk/cli/commands/agents.py +45 -107
- glaip_sdk/cli/commands/configure.py +12 -36
- glaip_sdk/cli/commands/mcps.py +26 -63
- glaip_sdk/cli/commands/models.py +2 -4
- glaip_sdk/cli/commands/tools.py +22 -35
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +1 -3
- glaip_sdk/cli/display.py +4 -12
- glaip_sdk/cli/io.py +8 -14
- glaip_sdk/cli/main.py +10 -30
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +3 -9
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/agent_session.py +5 -10
- glaip_sdk/cli/slash/prompt.py +3 -10
- glaip_sdk/cli/slash/session.py +46 -95
- glaip_sdk/cli/transcript/cache.py +6 -19
- glaip_sdk/cli/transcript/capture.py +6 -20
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +11 -40
- glaip_sdk/cli/update_notifier.py +165 -21
- glaip_sdk/cli/utils.py +33 -84
- glaip_sdk/cli/validators.py +11 -12
- glaip_sdk/client/_agent_payloads.py +10 -30
- glaip_sdk/client/agents.py +33 -63
- glaip_sdk/client/base.py +77 -35
- glaip_sdk/client/mcps.py +1 -3
- glaip_sdk/client/run_rendering.py +6 -14
- glaip_sdk/client/tools.py +8 -24
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/models.py +14 -33
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/client_utils.py +7 -21
- glaip_sdk/utils/display.py +2 -6
- glaip_sdk/utils/general.py +1 -3
- glaip_sdk/utils/import_export.py +3 -9
- glaip_sdk/utils/rendering/formatting.py +2 -5
- glaip_sdk/utils/rendering/models.py +2 -6
- glaip_sdk/utils/rendering/renderer/__init__.py +1 -3
- glaip_sdk/utils/rendering/renderer/base.py +63 -189
- glaip_sdk/utils/rendering/renderer/debug.py +4 -14
- glaip_sdk/utils/rendering/renderer/panels.py +1 -3
- glaip_sdk/utils/rendering/renderer/progress.py +3 -11
- glaip_sdk/utils/rendering/renderer/stream.py +7 -19
- glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
- glaip_sdk/utils/rendering/step_tree_state.py +1 -3
- glaip_sdk/utils/rendering/steps.py +29 -83
- glaip_sdk/utils/resource_refs.py +4 -13
- glaip_sdk/utils/serialization.py +14 -46
- glaip_sdk/utils/validation.py +4 -4
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.1.2.dist-info}/METADATA +1 -1
- glaip_sdk-0.1.2.dist-info/RECORD +82 -0
- glaip_sdk-0.1.0.dist-info/RECORD +0 -82
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.1.2.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -124,9 +124,7 @@ def _json_default(value: Any) -> Any:
|
|
|
124
124
|
return repr(value)
|
|
125
125
|
|
|
126
126
|
|
|
127
|
-
def _write_manifest(
|
|
128
|
-
entries: Iterable[dict[str, Any]], cache_dir: Path | None = None
|
|
129
|
-
) -> None:
|
|
127
|
+
def _write_manifest(entries: Iterable[dict[str, Any]], cache_dir: Path | None = None) -> None:
|
|
130
128
|
path = manifest_path(cache_dir)
|
|
131
129
|
with path.open("w", encoding="utf-8") as fh:
|
|
132
130
|
for entry in entries:
|
|
@@ -212,8 +210,7 @@ def latest_manifest_entry(cache_dir: Path | None = None) -> dict[str, Any] | Non
|
|
|
212
210
|
return None
|
|
213
211
|
return max(
|
|
214
212
|
entries,
|
|
215
|
-
key=lambda e: _parse_iso(e.get("created_at"))
|
|
216
|
-
or datetime.min.replace(tzinfo=timezone.utc),
|
|
213
|
+
key=lambda e: _parse_iso(e.get("created_at")) or datetime.min.replace(tzinfo=timezone.utc),
|
|
217
214
|
)
|
|
218
215
|
|
|
219
216
|
|
|
@@ -237,11 +234,7 @@ def export_transcript(
|
|
|
237
234
|
) -> Path:
|
|
238
235
|
"""Copy a cached transcript to the requested destination path."""
|
|
239
236
|
directory = ensure_cache_dir(cache_dir)
|
|
240
|
-
entry = (
|
|
241
|
-
resolve_manifest_entry(run_id, directory)
|
|
242
|
-
if run_id
|
|
243
|
-
else latest_manifest_entry(directory)
|
|
244
|
-
)
|
|
237
|
+
entry = resolve_manifest_entry(run_id, directory) if run_id else latest_manifest_entry(directory)
|
|
245
238
|
if entry is None:
|
|
246
239
|
raise FileNotFoundError("No cached transcripts available for export.")
|
|
247
240
|
|
|
@@ -259,9 +252,7 @@ def export_transcript(
|
|
|
259
252
|
lines = cache_file.read_text(encoding="utf-8").splitlines()
|
|
260
253
|
records = [json.loads(line) for line in lines if line.strip()]
|
|
261
254
|
except json.JSONDecodeError as exc:
|
|
262
|
-
raise FileNotFoundError(
|
|
263
|
-
f"Cached transcript file is corrupted: {cache_file}"
|
|
264
|
-
) from exc
|
|
255
|
+
raise FileNotFoundError(f"Cached transcript file is corrupted: {cache_file}") from exc
|
|
265
256
|
|
|
266
257
|
with destination.open("w", encoding="utf-8") as fh:
|
|
267
258
|
for idx, record in enumerate(records):
|
|
@@ -276,12 +267,8 @@ def export_transcript(
|
|
|
276
267
|
def suggest_filename(entry: dict[str, Any] | None = None) -> str:
|
|
277
268
|
"""Return a friendly filename suggestion for exporting a transcript."""
|
|
278
269
|
run_id = entry.get("run_id") if entry else uuid.uuid4().hex
|
|
279
|
-
created_at = (
|
|
280
|
-
|
|
281
|
-
)
|
|
282
|
-
timestamp = (
|
|
283
|
-
created_at.replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
|
|
284
|
-
)
|
|
270
|
+
created_at = entry.get("created_at") if entry else datetime.now(timezone.utc).isoformat()
|
|
271
|
+
timestamp = created_at.replace(":", "").replace("-", "").replace("T", "_").split("+")[0]
|
|
285
272
|
return f"aip-run-{timestamp}-{run_id}.jsonl"
|
|
286
273
|
|
|
287
274
|
|
|
@@ -65,11 +65,7 @@ def compute_finished_at(renderer: Any) -> float | None:
|
|
|
65
65
|
|
|
66
66
|
if started_at is None:
|
|
67
67
|
stream_processor = getattr(renderer, "stream_processor", None)
|
|
68
|
-
started_at = (
|
|
69
|
-
getattr(stream_processor, "streaming_started_at", None)
|
|
70
|
-
if stream_processor is not None
|
|
71
|
-
else None
|
|
72
|
-
)
|
|
68
|
+
started_at = getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
|
|
73
69
|
if started_at is None or duration is None:
|
|
74
70
|
return None
|
|
75
71
|
try:
|
|
@@ -78,9 +74,7 @@ def compute_finished_at(renderer: Any) -> float | None:
|
|
|
78
74
|
return None
|
|
79
75
|
|
|
80
76
|
|
|
81
|
-
def extract_server_run_id(
|
|
82
|
-
meta: dict[str, Any], events: list[dict[str, Any]]
|
|
83
|
-
) -> str | None:
|
|
77
|
+
def extract_server_run_id(meta: dict[str, Any], events: list[dict[str, Any]]) -> str | None:
|
|
84
78
|
"""Derive a server-side run identifier from renderer metadata."""
|
|
85
79
|
run_id = meta.get("run_id") or meta.get("id")
|
|
86
80
|
if run_id:
|
|
@@ -107,9 +101,7 @@ def _coerce_meta(meta: Any) -> dict[str, Any]:
|
|
|
107
101
|
return {"value": coerce_result_text(meta)}
|
|
108
102
|
|
|
109
103
|
|
|
110
|
-
def register_last_transcript(
|
|
111
|
-
ctx: Any, payload: TranscriptPayload, store_result: TranscriptStoreResult
|
|
112
|
-
) -> None:
|
|
104
|
+
def register_last_transcript(ctx: Any, payload: TranscriptPayload, store_result: TranscriptStoreResult) -> None:
|
|
113
105
|
"""Persist last-run transcript references onto the Click context."""
|
|
114
106
|
ctx_obj = getattr(ctx, "obj", None)
|
|
115
107
|
if not isinstance(ctx_obj, dict):
|
|
@@ -213,9 +205,7 @@ def _derive_transcript_meta(
|
|
|
213
205
|
|
|
214
206
|
stream_processor = getattr(renderer, "stream_processor", None)
|
|
215
207
|
stream_started_at = (
|
|
216
|
-
getattr(stream_processor, "streaming_started_at", None)
|
|
217
|
-
if stream_processor is not None
|
|
218
|
-
else None
|
|
208
|
+
getattr(stream_processor, "streaming_started_at", None) if stream_processor is not None else None
|
|
219
209
|
)
|
|
220
210
|
finished_at = compute_finished_at(renderer)
|
|
221
211
|
model_name = meta.get("model") or model
|
|
@@ -236,16 +226,12 @@ def store_transcript_for_session(
|
|
|
236
226
|
if not hasattr(renderer, "get_transcript_events"):
|
|
237
227
|
return None
|
|
238
228
|
|
|
239
|
-
events, aggregated_output, final_output = _collect_renderer_outputs(
|
|
240
|
-
renderer, final_result
|
|
241
|
-
)
|
|
229
|
+
events, aggregated_output, final_output = _collect_renderer_outputs(renderer, final_result)
|
|
242
230
|
|
|
243
231
|
if not (events or aggregated_output or final_output):
|
|
244
232
|
return None
|
|
245
233
|
|
|
246
|
-
meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(
|
|
247
|
-
renderer, model
|
|
248
|
-
)
|
|
234
|
+
meta, stream_started_at, finished_at, model_name = _derive_transcript_meta(renderer, model)
|
|
249
235
|
|
|
250
236
|
payload: TranscriptPayload = build_transcript_payload(
|
|
251
237
|
events=events,
|
|
@@ -20,9 +20,7 @@ from glaip_sdk.cli.transcript.capture import StoredTranscriptContext
|
|
|
20
20
|
from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def should_launch_post_run_viewer(
|
|
24
|
-
ctx: Any, console: Console, *, slash_mode: bool
|
|
25
|
-
) -> bool:
|
|
23
|
+
def should_launch_post_run_viewer(ctx: Any, console: Console, *, slash_mode: bool) -> bool:
|
|
26
24
|
"""Return True if the viewer should open automatically."""
|
|
27
25
|
if slash_mode:
|
|
28
26
|
return False
|
|
@@ -72,9 +72,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
72
72
|
|
|
73
73
|
def run(self) -> None:
|
|
74
74
|
"""Enter the interactive loop."""
|
|
75
|
-
if not self.ctx.events and not (
|
|
76
|
-
self.ctx.default_output or self.ctx.final_output
|
|
77
|
-
):
|
|
75
|
+
if not self.ctx.events and not (self.ctx.default_output or self.ctx.final_output):
|
|
78
76
|
return
|
|
79
77
|
if self._view_mode == "transcript":
|
|
80
78
|
self._render()
|
|
@@ -91,9 +89,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
91
89
|
except Exception: # pragma: no cover - platform quirks
|
|
92
90
|
pass
|
|
93
91
|
|
|
94
|
-
header = (
|
|
95
|
-
f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
96
|
-
)
|
|
92
|
+
header = f"Agent transcript viewer · run {self.ctx.manifest_entry.get('run_id')}"
|
|
97
93
|
agent_label = self.ctx.manifest_entry.get("agent_name") or "unknown agent"
|
|
98
94
|
model = self.ctx.manifest_entry.get("model") or self.ctx.meta.get("model")
|
|
99
95
|
agent_id = self.ctx.manifest_entry.get("agent_id")
|
|
@@ -134,9 +130,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
134
130
|
self._render_final_panel()
|
|
135
131
|
|
|
136
132
|
self.console.print("[bold]Transcript Events[/bold]")
|
|
137
|
-
self.console.print(
|
|
138
|
-
"[dim]────────────────────────────────────────────────────────[/dim]"
|
|
139
|
-
)
|
|
133
|
+
self.console.print("[dim]────────────────────────────────────────────────────────[/dim]")
|
|
140
134
|
|
|
141
135
|
base_received_ts: datetime | None = None
|
|
142
136
|
for event in self.ctx.events:
|
|
@@ -152,11 +146,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
152
146
|
self.console.print()
|
|
153
147
|
|
|
154
148
|
def _render_final_panel(self) -> None:
|
|
155
|
-
content =
|
|
156
|
-
self.ctx.final_output
|
|
157
|
-
or self.ctx.default_output
|
|
158
|
-
or "No response content captured."
|
|
159
|
-
)
|
|
149
|
+
content = self.ctx.final_output or self.ctx.default_output or "No response content captured."
|
|
160
150
|
title = "Final Result"
|
|
161
151
|
duration_text = self._extract_final_duration()
|
|
162
152
|
if duration_text:
|
|
@@ -218,9 +208,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
218
208
|
raw = str(path)
|
|
219
209
|
return raw if len(raw) <= 80 else f"…{raw[-77:]}"
|
|
220
210
|
|
|
221
|
-
selection = self._prompt_export_choice(
|
|
222
|
-
default_path, _display_path(default_path)
|
|
223
|
-
)
|
|
211
|
+
selection = self._prompt_export_choice(default_path, _display_path(default_path))
|
|
224
212
|
if selection is None:
|
|
225
213
|
self._legacy_export_prompt(default_path, _display_path)
|
|
226
214
|
return
|
|
@@ -246,9 +234,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
246
234
|
except Exception as exc: # pragma: no cover - unexpected IO failures
|
|
247
235
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
248
236
|
|
|
249
|
-
def _prompt_export_choice(
|
|
250
|
-
self, default_path: Path, default_display: str
|
|
251
|
-
) -> tuple[str, Any] | None:
|
|
237
|
+
def _prompt_export_choice(self, default_path: Path, default_display: str) -> tuple[str, Any] | None:
|
|
252
238
|
"""Render interactive export menu with numeric shortcuts."""
|
|
253
239
|
if not self.console.is_terminal or questionary is None or Choice is None:
|
|
254
240
|
return None
|
|
@@ -305,9 +291,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
305
291
|
candidate = Path.cwd() / candidate
|
|
306
292
|
return candidate
|
|
307
293
|
|
|
308
|
-
def _legacy_export_prompt(
|
|
309
|
-
self, default_path: Path, formatter: Callable[[Path], str]
|
|
310
|
-
) -> None:
|
|
294
|
+
def _legacy_export_prompt(self, default_path: Path, formatter: Callable[[Path], str]) -> None:
|
|
311
295
|
"""Fallback export workflow when interactive UI is unavailable."""
|
|
312
296
|
self.console.print("[dim]Export options (fallback mode)[/dim]")
|
|
313
297
|
self.console.print(f" 1. Save to default ({formatter(default_path)})")
|
|
@@ -353,20 +337,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
353
337
|
self.console.print(f"[red]Failed to export transcript: {exc}[/red]")
|
|
354
338
|
|
|
355
339
|
def _print_command_hint(self) -> None:
|
|
356
|
-
self.console.print(
|
|
357
|
-
"[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]"
|
|
358
|
-
)
|
|
340
|
+
self.console.print("[dim]Ctrl+T to toggle transcript · type `e` to export · press Enter to exit[/dim]")
|
|
359
341
|
self.console.print()
|
|
360
342
|
|
|
361
343
|
def _get_user_query(self) -> str | None:
|
|
362
344
|
meta = self.ctx.meta or {}
|
|
363
345
|
manifest = self.ctx.manifest_entry or {}
|
|
364
|
-
return (
|
|
365
|
-
meta.get("input_message")
|
|
366
|
-
or meta.get("query")
|
|
367
|
-
or meta.get("message")
|
|
368
|
-
or manifest.get("input_message")
|
|
369
|
-
)
|
|
346
|
+
return meta.get("input_message") or meta.get("query") or meta.get("message") or manifest.get("input_message")
|
|
370
347
|
|
|
371
348
|
def _render_user_query(self, query: str) -> None:
|
|
372
349
|
panel = AIPPanel(
|
|
@@ -730,11 +707,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
730
707
|
status = metadata.get("status")
|
|
731
708
|
event_time = metadata.get("time")
|
|
732
709
|
|
|
733
|
-
if (
|
|
734
|
-
status == "running"
|
|
735
|
-
and step.get("started_at") is None
|
|
736
|
-
and isinstance(event_time, (int, float))
|
|
737
|
-
):
|
|
710
|
+
if status == "running" and step.get("started_at") is None and isinstance(event_time, (int, float)):
|
|
738
711
|
try:
|
|
739
712
|
step["started_at"] = float(event_time)
|
|
740
713
|
except Exception:
|
|
@@ -760,9 +733,7 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
760
733
|
started_at = step.get("started_at")
|
|
761
734
|
duration_value: float | None = None
|
|
762
735
|
|
|
763
|
-
if isinstance(event_time, (int, float)) and isinstance(
|
|
764
|
-
started_at, (int, float)
|
|
765
|
-
):
|
|
736
|
+
if isinstance(event_time, (int, float)) and isinstance(started_at, (int, float)):
|
|
766
737
|
try:
|
|
767
738
|
delta = float(event_time) - float(started_at)
|
|
768
739
|
if delta >= 0:
|
glaip_sdk/cli/update_notifier.py
CHANGED
|
@@ -6,10 +6,14 @@ Author:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import importlib
|
|
10
|
+
import logging
|
|
9
11
|
import os
|
|
10
|
-
from collections.abc import Callable
|
|
12
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
13
|
+
from contextlib import contextmanager
|
|
11
14
|
from typing import Any, Literal
|
|
12
15
|
|
|
16
|
+
import click
|
|
13
17
|
import httpx
|
|
14
18
|
from packaging.version import InvalidVersion, Version
|
|
15
19
|
from rich import box
|
|
@@ -17,9 +21,11 @@ from rich.console import Console
|
|
|
17
21
|
|
|
18
22
|
from glaip_sdk.branding import (
|
|
19
23
|
ACCENT_STYLE,
|
|
24
|
+
ERROR_STYLE,
|
|
20
25
|
SUCCESS_STYLE,
|
|
21
26
|
WARNING_STYLE,
|
|
22
27
|
)
|
|
28
|
+
from glaip_sdk.cli.commands.update import update_command
|
|
23
29
|
from glaip_sdk.cli.utils import command_hint, format_command_hint
|
|
24
30
|
from glaip_sdk.rich_components import AIPPanel
|
|
25
31
|
|
|
@@ -28,6 +34,8 @@ FetchLatestVersion = Callable[[], str | None]
|
|
|
28
34
|
PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
|
|
29
35
|
DEFAULT_TIMEOUT = 1.5 # seconds
|
|
30
36
|
|
|
37
|
+
_LOGGER = logging.getLogger(__name__)
|
|
38
|
+
|
|
31
39
|
|
|
32
40
|
def _parse_version(value: str) -> Version | None:
|
|
33
41
|
"""Parse a version string into a `Version`, returning None on failure."""
|
|
@@ -43,13 +51,16 @@ def _fetch_latest_version(package_name: str) -> str | None:
|
|
|
43
51
|
timeout = httpx.Timeout(DEFAULT_TIMEOUT)
|
|
44
52
|
|
|
45
53
|
try:
|
|
46
|
-
with
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
54
|
+
with _suppress_library_logging():
|
|
55
|
+
with httpx.Client(timeout=timeout) as client:
|
|
56
|
+
response = client.get(url, headers={"Accept": "application/json"})
|
|
57
|
+
response.raise_for_status()
|
|
58
|
+
payload = response.json()
|
|
59
|
+
except httpx.HTTPError as exc:
|
|
60
|
+
_LOGGER.debug("Update check failed: %s", exc, exc_info=True)
|
|
51
61
|
return None
|
|
52
|
-
except ValueError:
|
|
62
|
+
except ValueError as exc:
|
|
63
|
+
_LOGGER.debug("Invalid JSON while checking for updates: %s", exc, exc_info=True)
|
|
53
64
|
return None
|
|
54
65
|
|
|
55
66
|
info = payload.get("info") if isinstance(payload, dict) else None
|
|
@@ -68,6 +79,8 @@ def _build_update_panel(
|
|
|
68
79
|
current_version: str,
|
|
69
80
|
latest_version: str,
|
|
70
81
|
command_text: str,
|
|
82
|
+
*,
|
|
83
|
+
show_command_hint: bool,
|
|
71
84
|
) -> AIPPanel:
|
|
72
85
|
"""Create a Rich panel that prompts the user to update."""
|
|
73
86
|
command_markup = format_command_hint(command_text) or command_text
|
|
@@ -75,9 +88,10 @@ def _build_update_panel(
|
|
|
75
88
|
f"[{WARNING_STYLE}]✨ Update available![/] "
|
|
76
89
|
f"{current_version} → {latest_version}\n\n"
|
|
77
90
|
"See the latest release notes:\n"
|
|
78
|
-
f"https://pypi.org/project/glaip-sdk/{latest_version}
|
|
79
|
-
f"[{ACCENT_STYLE}]Run[/] {command_markup} to install."
|
|
91
|
+
f"https://pypi.org/project/glaip-sdk/{latest_version}/"
|
|
80
92
|
)
|
|
93
|
+
if show_command_hint:
|
|
94
|
+
message += f"\n\n[{ACCENT_STYLE}]Run[/] {command_markup} to install."
|
|
81
95
|
return AIPPanel(
|
|
82
96
|
message,
|
|
83
97
|
title=f"[{SUCCESS_STYLE}]AIP SDK Update[/]",
|
|
@@ -97,11 +111,7 @@ def maybe_notify_update(
|
|
|
97
111
|
slash_command: str | None = None,
|
|
98
112
|
style: Literal["panel", "inline"] = "panel",
|
|
99
113
|
) -> None:
|
|
100
|
-
"""Check PyPI for a newer version and display a prompt if one exists.
|
|
101
|
-
|
|
102
|
-
This function deliberately swallows network errors to avoid impacting CLI
|
|
103
|
-
startup time when offline or when PyPI is unavailable.
|
|
104
|
-
"""
|
|
114
|
+
"""Check PyPI for a newer version and display a prompt if one exists."""
|
|
105
115
|
if not _should_check_for_updates():
|
|
106
116
|
return
|
|
107
117
|
|
|
@@ -120,18 +130,152 @@ def maybe_notify_update(
|
|
|
120
130
|
return
|
|
121
131
|
|
|
122
132
|
active_console = console or Console()
|
|
133
|
+
should_prompt = _should_prompt_for_action(active_console, ctx)
|
|
134
|
+
|
|
123
135
|
if style == "inline":
|
|
136
|
+
if should_prompt:
|
|
137
|
+
message = (
|
|
138
|
+
f"[{WARNING_STYLE}]✨ Update[/] "
|
|
139
|
+
f"{current_version} → {latest_version} "
|
|
140
|
+
"- choose Update now or Skip to continue."
|
|
141
|
+
)
|
|
142
|
+
active_console.print(message)
|
|
143
|
+
_handle_update_decision(active_console, ctx)
|
|
144
|
+
return
|
|
145
|
+
|
|
124
146
|
command_markup = format_command_hint(command_text) or command_text
|
|
125
|
-
|
|
126
|
-
f"[{WARNING_STYLE}]✨ Update[/] "
|
|
127
|
-
f"{current_version} → {latest_version} "
|
|
128
|
-
f"- {command_markup}"
|
|
129
|
-
)
|
|
130
|
-
active_console.print(message)
|
|
147
|
+
active_console.print(f"[{WARNING_STYLE}]✨ Update[/] {current_version} → {latest_version} - {command_markup}")
|
|
131
148
|
return
|
|
132
149
|
|
|
133
|
-
panel = _build_update_panel(
|
|
150
|
+
panel = _build_update_panel(
|
|
151
|
+
current_version,
|
|
152
|
+
latest_version,
|
|
153
|
+
command_text,
|
|
154
|
+
show_command_hint=not should_prompt,
|
|
155
|
+
)
|
|
134
156
|
active_console.print(panel)
|
|
157
|
+
if should_prompt:
|
|
158
|
+
_handle_update_decision(active_console, ctx)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _handle_update_decision(console: Console, ctx: Any) -> None:
|
|
162
|
+
"""Prompt the user to take action on the available update."""
|
|
163
|
+
choice = _prompt_update_decision(console)
|
|
164
|
+
if choice == "skip":
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
_run_update_command(console, ctx)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _should_prompt_for_action(console: Console, ctx: Any | None) -> bool:
|
|
171
|
+
"""Return True when we can safely block for interactive input."""
|
|
172
|
+
if ctx is None or not hasattr(ctx, "invoke"):
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
is_interactive = getattr(console, "is_interactive", False)
|
|
176
|
+
if not isinstance(is_interactive, bool) or not is_interactive:
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
is_terminal = getattr(console, "is_terminal", False)
|
|
180
|
+
if not isinstance(is_terminal, bool) or not is_terminal:
|
|
181
|
+
return False
|
|
182
|
+
|
|
183
|
+
input_method = getattr(console, "input", None)
|
|
184
|
+
return callable(input_method)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
|
|
188
|
+
"""Ask the user to choose between updating now or skipping."""
|
|
189
|
+
console.print(
|
|
190
|
+
f"[{ACCENT_STYLE}]Select an option to continue:[/]\n"
|
|
191
|
+
f" [{SUCCESS_STYLE}]1.[/] Update now\n"
|
|
192
|
+
f" [{WARNING_STYLE}]2.[/] Skip\n"
|
|
193
|
+
)
|
|
194
|
+
console.print("[dim]Press Enter after typing your choice.[/]")
|
|
195
|
+
|
|
196
|
+
while True:
|
|
197
|
+
try:
|
|
198
|
+
response = console.input("Choice [1/2]: ").strip().lower()
|
|
199
|
+
except (KeyboardInterrupt, EOFError):
|
|
200
|
+
console.print(f"\n[{WARNING_STYLE}]Update skipped.[/]")
|
|
201
|
+
return "skip"
|
|
202
|
+
|
|
203
|
+
if response in {"1", "update", "u"}:
|
|
204
|
+
return "update"
|
|
205
|
+
if response in {"2", "skip", "s"}:
|
|
206
|
+
return "skip"
|
|
207
|
+
|
|
208
|
+
console.print(f"[{ERROR_STYLE}]Please enter 1 to update now or 2 to skip.[/]")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _run_update_command(console: Console, ctx: Any) -> None:
|
|
212
|
+
"""Invoke the built-in update command and surface any errors."""
|
|
213
|
+
try:
|
|
214
|
+
ctx.invoke(update_command)
|
|
215
|
+
except click.ClickException as exc:
|
|
216
|
+
exc.show()
|
|
217
|
+
console.print(f"[{ERROR_STYLE}]Update command exited with an error.[/]")
|
|
218
|
+
except click.Abort:
|
|
219
|
+
console.print(f"[{WARNING_STYLE}]Update aborted by user.[/]")
|
|
220
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
221
|
+
console.print(f"[{ERROR_STYLE}]Unexpected error while running update: {exc}[/]")
|
|
222
|
+
else:
|
|
223
|
+
_refresh_installed_version(console, ctx)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@contextmanager
|
|
227
|
+
def _suppress_library_logging(
|
|
228
|
+
logger_names: Iterable[str] | None = None, *, level: int = logging.WARNING
|
|
229
|
+
) -> Iterator[None]:
|
|
230
|
+
"""Temporarily raise log level for selected libraries during update checks."""
|
|
231
|
+
names = tuple(logger_names) if logger_names is not None else ("httpx",)
|
|
232
|
+
captured: list[tuple[logging.Logger, int]] = []
|
|
233
|
+
try:
|
|
234
|
+
for name in names:
|
|
235
|
+
logger = logging.getLogger(name)
|
|
236
|
+
captured.append((logger, logger.level))
|
|
237
|
+
logger.setLevel(level)
|
|
238
|
+
yield
|
|
239
|
+
finally:
|
|
240
|
+
for logger, previous_level in captured:
|
|
241
|
+
logger.setLevel(previous_level)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _refresh_installed_version(console: Console, ctx: Any) -> None:
|
|
245
|
+
"""Reload runtime metadata after an in-process upgrade."""
|
|
246
|
+
new_version: str | None = None
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
|
|
250
|
+
new_version = getattr(version_module, "__version__", None)
|
|
251
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
252
|
+
_LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
branding_module = importlib.import_module("glaip_sdk.branding")
|
|
256
|
+
if new_version:
|
|
257
|
+
branding_module.SDK_VERSION = new_version
|
|
258
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
259
|
+
_LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
|
|
260
|
+
|
|
261
|
+
session = _get_slash_session(ctx)
|
|
262
|
+
if session and hasattr(session, "refresh_branding"):
|
|
263
|
+
try:
|
|
264
|
+
session.refresh_branding(new_version)
|
|
265
|
+
return
|
|
266
|
+
except Exception as exc: # pragma: no cover - defensive guard
|
|
267
|
+
_LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
|
|
268
|
+
|
|
269
|
+
if new_version:
|
|
270
|
+
console.print(f"[{SUCCESS_STYLE}]CLI now running glaip-sdk {new_version}.[/]")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _get_slash_session(ctx: Any) -> Any | None:
|
|
274
|
+
"""Return active slash session from the Click context if present."""
|
|
275
|
+
ctx_obj = getattr(ctx, "obj", None)
|
|
276
|
+
if isinstance(ctx_obj, dict):
|
|
277
|
+
return ctx_obj.get("_slash_session")
|
|
278
|
+
return None
|
|
135
279
|
|
|
136
280
|
|
|
137
281
|
__all__ = ["maybe_notify_update"]
|