glaip-sdk 0.0.7__py3-none-any.whl → 0.0.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.
glaip_sdk/cli/utils.py CHANGED
@@ -8,13 +8,16 @@ from __future__ import annotations
8
8
 
9
9
  import io
10
10
  import json
11
+ import logging
11
12
  import os
12
13
  import platform
13
14
  import shlex
14
15
  import shutil
15
16
  import subprocess
17
+ import sys
16
18
  import tempfile
17
19
  from collections.abc import Callable
20
+ from contextlib import AbstractContextManager, nullcontext
18
21
  from pathlib import Path
19
22
  from typing import TYPE_CHECKING, Any
20
23
 
@@ -50,6 +53,7 @@ from glaip_sdk.utils.rendering.renderer import (
50
53
  )
51
54
 
52
55
  console = Console()
56
+ logger = logging.getLogger("glaip_sdk.cli.utils")
53
57
 
54
58
 
55
59
  # ----------------------------- Context helpers ---------------------------- #
@@ -225,6 +229,76 @@ def _get_view(ctx: Any) -> str:
225
229
  return fallback or "rich"
226
230
 
227
231
 
232
+ def spinner_context(
233
+ ctx: Any | None,
234
+ message: str,
235
+ *,
236
+ console_override: Console | None = None,
237
+ spinner: str = "dots",
238
+ spinner_style: str = "cyan",
239
+ ) -> AbstractContextManager[Any]:
240
+ """Return a context manager that renders a spinner when appropriate."""
241
+
242
+ active_console = console_override or console
243
+ if not _can_use_spinner(ctx, active_console):
244
+ return nullcontext()
245
+
246
+ return active_console.status(
247
+ message,
248
+ spinner=spinner,
249
+ spinner_style=spinner_style,
250
+ )
251
+
252
+
253
+ def _can_use_spinner(ctx: Any | None, active_console: Console) -> bool:
254
+ """Check if spinner output is allowed in the current environment."""
255
+
256
+ if ctx is not None:
257
+ tty_enabled = bool(get_ctx_value(ctx, "tty", True))
258
+ view = (_get_view(ctx) or "rich").lower()
259
+ if not tty_enabled or view not in {"", "rich"}:
260
+ return False
261
+
262
+ if not active_console.is_terminal:
263
+ return False
264
+
265
+ return _stream_supports_tty(getattr(active_console, "file", None))
266
+
267
+
268
+ def _stream_supports_tty(stream: Any) -> bool:
269
+ """Return True if the provided stream can safely render a spinner."""
270
+
271
+ target = stream if hasattr(stream, "isatty") else sys.stdout
272
+ try:
273
+ return bool(target.isatty())
274
+ except Exception:
275
+ return False
276
+
277
+
278
+ def _spinner_update(status_indicator: Any | None, message: str) -> None:
279
+ """Update spinner text when a status indicator is active."""
280
+
281
+ if status_indicator is None:
282
+ return
283
+
284
+ try:
285
+ status_indicator.update(message)
286
+ except Exception: # pragma: no cover - defensive update
287
+ pass
288
+
289
+
290
+ def _spinner_stop(status_indicator: Any | None) -> None:
291
+ """Stop an active spinner safely."""
292
+
293
+ if status_indicator is None:
294
+ return
295
+
296
+ try:
297
+ status_indicator.stop()
298
+ except Exception: # pragma: no cover - defensive stop
299
+ pass
300
+
301
+
228
302
  # ----------------------------- Client config ----------------------------- #
229
303
 
230
304
 
@@ -507,6 +581,8 @@ def _fuzzy_pick(
507
581
  )
508
582
  except (KeyboardInterrupt, EOFError): # pragma: no cover - user cancelled input
509
583
  return None
584
+ except Exception: # pragma: no cover - prompt_toolkit not available in headless env
585
+ return None
510
586
 
511
587
  return _perform_fuzzy_search(answer, labels, by_label) if answer else None
512
588
 
@@ -798,11 +874,14 @@ def _handle_fuzzy_pick_selection(
798
874
  rows: list[dict[str, Any]], columns: list[tuple], title: str
799
875
  ) -> bool:
800
876
  """Handle fuzzy picker selection, returns True if selection was made."""
801
- picked = (
802
- _fuzzy_pick(rows, columns, title)
803
- if console.is_terminal and os.isatty(1)
804
- else None
805
- )
877
+ picked = None
878
+ if console.is_terminal and os.isatty(1):
879
+ try:
880
+ picked = _fuzzy_pick(rows, columns, title)
881
+ except Exception:
882
+ logger.debug(
883
+ "Fuzzy picker failed; falling back to table output", exc_info=True
884
+ )
806
885
  if picked:
807
886
  table = _create_table(columns, title)
808
887
  table.add_row(*[str(picked.get(key, "N/A")) for key, _, _, _ in columns])
@@ -1096,6 +1175,7 @@ def resolve_resource(
1096
1175
  label: str,
1097
1176
  select: int | None = None,
1098
1177
  interface_preference: str = "fuzzy",
1178
+ status_indicator: Any | None = None,
1099
1179
  ) -> Any | None:
1100
1180
  """Resolve resource reference (ID or name) with ambiguity handling.
1101
1181
 
@@ -1107,32 +1187,46 @@ def resolve_resource(
1107
1187
  label: Resource type label for error messages
1108
1188
  select: Optional selection index for ambiguity resolution
1109
1189
  interface_preference: "fuzzy" for fuzzy picker, "questionary" for up/down list
1190
+ status_indicator: Optional Rich status indicator for wait animations
1110
1191
 
1111
1192
  Returns:
1112
1193
  Resolved resource object
1113
1194
  """
1195
+ spinner = status_indicator
1196
+ _spinner_update(spinner, f"[bold blue]Resolving {label}…[/bold blue]")
1197
+
1114
1198
  # Try to resolve by ID first
1199
+ _spinner_update(spinner, f"[bold blue]Fetching {label} by ID…[/bold blue]")
1115
1200
  result = _resolve_by_id(ref, get_by_id)
1116
1201
  if result is not None:
1202
+ _spinner_update(spinner, f"[bold green]{label} found[/bold green]")
1117
1203
  return result
1118
1204
 
1119
1205
  # If get_by_id returned None, the resource doesn't exist
1120
1206
  if is_uuid(ref):
1207
+ _spinner_stop(spinner)
1121
1208
  raise click.ClickException(f"{label} '{ref}' not found")
1122
1209
 
1123
1210
  # Find resources by name
1211
+ _spinner_update(
1212
+ spinner, f"[bold blue]Searching {label}s matching '{ref}'…[/bold blue]"
1213
+ )
1124
1214
  matches = find_by_name(name=ref)
1125
1215
  if not matches:
1216
+ _spinner_stop(spinner)
1126
1217
  raise click.ClickException(f"{label} '{ref}' not found")
1127
1218
 
1128
1219
  if len(matches) == 1:
1220
+ _spinner_update(spinner, f"[bold green]{label} found[/bold green]")
1129
1221
  return matches[0]
1130
1222
 
1131
1223
  # Multiple matches found, handle ambiguity
1132
1224
  if select:
1225
+ _spinner_stop(spinner)
1133
1226
  return _resolve_by_name_multiple_with_select(matches, select)
1134
1227
 
1135
1228
  # Choose interface based on preference
1229
+ _spinner_stop(spinner)
1136
1230
  if interface_preference == "fuzzy":
1137
1231
  return _resolve_by_name_multiple_fuzzy(ctx, ref, matches, label)
1138
1232
  else:
@@ -179,7 +179,7 @@ class RichStreamRenderer:
179
179
  AIPPanel(
180
180
  Markdown(f"**Query:** {query}"),
181
181
  title="User Request",
182
- border_style="yellow",
182
+ border_style="#d97706",
183
183
  padding=(0, 1),
184
184
  )
185
185
  )
@@ -311,6 +311,24 @@ class RichStreamRenderer:
311
311
  def _ensure_live(self) -> None:
312
312
  """Ensure live display is updated."""
313
313
  # Lazily create Live if needed
314
+ # Rich's Live expects the console to maintain a _live_stack list. When tests
315
+ # or callers provide a lightweight console double (e.g. unittest.mock.Mock),
316
+ # the attribute might be missing or replaced with another type which breaks
317
+ # the background refresh thread. Normal Rich consoles always expose
318
+ # _live_stack as a list, so we defensively initialise it if needed.
319
+ live_stack = getattr(self.console, "_live_stack", None)
320
+ if not isinstance(live_stack, list):
321
+ try:
322
+ self.console._live_stack = [] # type: ignore[attr-defined]
323
+ except Exception:
324
+ # If the console forbids attribute assignment we simply skip the
325
+ # live update for this cycle and fall back to buffered printing.
326
+ logger.debug(
327
+ "Console missing _live_stack; skipping live UI initialisation",
328
+ exc_info=True,
329
+ )
330
+ return
331
+
314
332
  if self.live is None and self.cfg.live:
315
333
  try:
316
334
  self.live = Live(
@@ -8,7 +8,7 @@ Authors:
8
8
  """
9
9
 
10
10
  import json
11
- from collections.abc import Iterable
11
+ from collections.abc import Callable, Iterable
12
12
  from pathlib import Path
13
13
  from typing import Any
14
14
 
@@ -217,13 +217,13 @@ def _coerce_resource_to_mapping(resource: Any) -> dict[str, Any] | None:
217
217
  if isinstance(resource, dict):
218
218
  return resource
219
219
 
220
- if hasattr(resource, "__dict__"):
221
- try:
220
+ try:
221
+ if hasattr(resource, "__dict__"):
222
222
  return dict(resource.__dict__)
223
- except (
224
- Exception
225
- ): # pragma: no cover - pathological objects can still defeat coercion
226
- return None
223
+ except (
224
+ Exception
225
+ ): # pragma: no cover - pathological objects can still defeat coercion
226
+ return None
227
227
 
228
228
  return None
229
229
 
@@ -235,27 +235,59 @@ def _iter_public_attribute_names(resource: Any) -> Iterable[str]:
235
235
  names: list[str] = []
236
236
 
237
237
  def _collect(candidates: Iterable[str] | None) -> None:
238
- if candidates is None:
239
- return
240
- for candidate in candidates:
238
+ for candidate in candidates or ():
241
239
  if candidate not in seen:
242
240
  seen.add(candidate)
243
241
  names.append(candidate)
244
242
 
245
- _collect(
246
- getattr(resource, "__dict__", {}).keys()
247
- if hasattr(resource, "__dict__")
248
- else None
249
- )
250
- _collect(getattr(resource, "__annotations__", {}).keys())
243
+ # Collect from __dict__
244
+ _collect_from_dict(resource, _collect)
245
+
246
+ # Collect from __annotations__
247
+ _collect_from_annotations(resource, _collect)
248
+
249
+ # Collect from __slots__
251
250
  _collect(getattr(resource, "__slots__", ()))
252
251
 
252
+ # Fallback to dir() if no names found
253
253
  if not names:
254
- _collect(name for name in dir(resource) if not name.startswith("__"))
254
+ _collect_from_dir(resource, _collect)
255
255
 
256
256
  return iter(names)
257
257
 
258
258
 
259
+ def _collect_from_dict(
260
+ resource: Any, collect_func: Callable[[Iterable[str]], None]
261
+ ) -> None:
262
+ """Safely collect attribute names from __dict__."""
263
+ try:
264
+ if hasattr(resource, "__dict__"):
265
+ dict_keys = getattr(resource, "__dict__", {})
266
+ if dict_keys:
267
+ collect_func(dict_keys.keys())
268
+ except Exception: # pragma: no cover - defensive programming
269
+ pass
270
+
271
+
272
+ def _collect_from_annotations(
273
+ resource: Any, collect_func: Callable[[Iterable[str]], None]
274
+ ) -> None:
275
+ """Safely collect attribute names from __annotations__."""
276
+ annotations = getattr(resource, "__annotations__", {})
277
+ if annotations:
278
+ collect_func(annotations.keys())
279
+
280
+
281
+ def _collect_from_dir(
282
+ resource: Any, collect_func: Callable[[Iterable[str]], None]
283
+ ) -> None:
284
+ """Safely collect attribute names from dir()."""
285
+ try:
286
+ collect_func(name for name in dir(resource) if not name.startswith("__"))
287
+ except Exception: # pragma: no cover - defensive programming
288
+ pass
289
+
290
+
259
291
  def _safe_getattr(resource: Any, name: str) -> Any:
260
292
  try:
261
293
  return getattr(resource, name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: glaip-sdk
3
- Version: 0.0.7
3
+ Version: 0.0.8
4
4
  Summary: Python SDK for GL AIP (GDP Labs AI Agent Package) - Simplified CLI Design
5
5
  License: MIT
6
6
  Author: Raymond Christopher
@@ -1,19 +1,23 @@
1
1
  glaip_sdk/__init__.py,sha256=FD-oTyFUKsTB9xTuGiqvkhuFXfeZ-TspjkeXERglha8,370
2
2
  glaip_sdk/_version.py,sha256=tGkFWAVu2ry4Hy7j-u7ophGbPRX8y-ngBbXDhN1VBIQ,2007
3
- glaip_sdk/branding.py,sha256=d-yGCYbUcdOlEH87E5PmGIyj6uBywsnf-Yd7zs__mUs,5371
3
+ glaip_sdk/branding.py,sha256=_TiQtiwH3ZSWaQhooWPHioriL0goTiYANeN6DT8lQX8,5398
4
4
  glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
5
5
  glaip_sdk/cli/agent_config.py,sha256=VHjebw68wAdhGUzYdPH8qz10oADZPRgUQcPW6F7iHIU,2421
6
6
  glaip_sdk/cli/commands/__init__.py,sha256=x0CZlZbZHoHvuzfoTWIyEch6WmNnbPzxajrox6riYp0,173
7
- glaip_sdk/cli/commands/agents.py,sha256=xiFKK76AeqkHyyQo7asdqNDy_VqbIzFHBUEvdE6lnwQ,36943
7
+ glaip_sdk/cli/commands/agents.py,sha256=1xGSoH6C9hSaGglXf4pnloqX5gJQIPsgHhkliiWQQcY,39988
8
8
  glaip_sdk/cli/commands/configure.py,sha256=eRDzsaKV4fl2lJt8ieS4g2-xRnaa02eAAPW8xBf-tqA,7507
9
9
  glaip_sdk/cli/commands/mcps.py,sha256=ZfmFkisXHHqgFH6WU7W5qPvfdt5Dxd5ZFZb_Ml3POoQ,12794
10
10
  glaip_sdk/cli/commands/models.py,sha256=lHEVuXT8Sq239C_2LneV8PimVZSm9DXJKajdSVO-N50,1496
11
11
  glaip_sdk/cli/commands/tools.py,sha256=Xm_qCe0h2EqhbqNuKTBvow1HMc7MbLYx6j92A6rLwu0,16743
12
- glaip_sdk/cli/display.py,sha256=Wr86LTdwUaaTceXwi2v5rJAYaWaYYYhnt3PinSSAQP8,8605
12
+ glaip_sdk/cli/display.py,sha256=jE20swoRKzpYUmc0jgbeonaXKeE9x95hfjWAEdnBYRc,8727
13
13
  glaip_sdk/cli/io.py,sha256=GPkw3pQMLBGoD5GH-KlbKpNRlVWFZOXHE17F7V3kQsI,3343
14
- glaip_sdk/cli/main.py,sha256=Z1uMlmU72FFXC2ushNTWpemhQWKbIJNFJauH6FOCPqA,11059
15
- glaip_sdk/cli/resolution.py,sha256=4PEW_KZfN13k7GGEWpx3dOTywE_d-ujSlUhO4liov5w,1673
16
- glaip_sdk/cli/utils.py,sha256=GQ_VwY374D8aLlySWYdExOZMap7Bb0nFIzS9gRhR5gs,37660
14
+ glaip_sdk/cli/main.py,sha256=1RSR3YmJECnpErD_y9NXqCHjyho3JkYkVc26J6NAbzE,12694
15
+ glaip_sdk/cli/resolution.py,sha256=BOw2NchReLKewAwBAZLWw_3_bI7u3tfzQEO7kQbIiGE,2067
16
+ glaip_sdk/cli/slash/__init__.py,sha256=Vdv6Y8bu-pA8dxDlyP4XrhudBPivztUozhLAz9vaLig,682
17
+ glaip_sdk/cli/slash/agent_session.py,sha256=JFIFBxB4xzigqHtpLGbl2fgg7RHNwy3e-kUMPMK9MdM,5006
18
+ glaip_sdk/cli/slash/prompt.py,sha256=QFWYgNt5AhBRYGGyUkH30ToZE7B6LS-eZGzoyibYrK8,6064
19
+ glaip_sdk/cli/slash/session.py,sha256=GtnnBqV79KtFJHeLfCKKnCXJ6SU1AmeXquPjy72T20E,24215
20
+ glaip_sdk/cli/utils.py,sha256=Jhjv9ggwZGQ8oJZGIPIpOc29LqSZAks9FLVWtTpklYU,40629
17
21
  glaip_sdk/cli/validators.py,sha256=USbBgY86AwuDHO-Q_g8g7hu-ot4NgITBsWjTWIl62ms,5569
18
22
  glaip_sdk/client/__init__.py,sha256=nYLXfBVTTWwKjP0e63iumPYO4k5FifwWaELQPaPIKIg,188
19
23
  glaip_sdk/client/agents.py,sha256=FSKubF40wptMNIheC3_iawiX2CRbhTcNLFiz4qkPC6k,34659
@@ -36,7 +40,7 @@ glaip_sdk/utils/rendering/__init__.py,sha256=vXjwk5rPhhfPyD8S0DnV4GFFEtPJp4HCCg1
36
40
  glaip_sdk/utils/rendering/formatting.py,sha256=_k8tkcobctmHvdygMljZF7-ALGXpD9-hHF1CNtM2KMU,7201
37
41
  glaip_sdk/utils/rendering/models.py,sha256=SS34_00FaoGuSYn-viBkAtIbq7cJNwwPjpxnvyeUmxI,1567
38
42
  glaip_sdk/utils/rendering/renderer/__init__.py,sha256=EXwVBmGkSYcype4ocAXo69Z1kXu0gpNXmhH5LW0_B7A,2939
39
- glaip_sdk/utils/rendering/renderer/base.py,sha256=4WdvYwsvCBtiIaB-LVteDORyxZN1HCT2jFfU0X0ReXY,39977
43
+ glaip_sdk/utils/rendering/renderer/base.py,sha256=OqRBfdE1cwYhaB-1wXwc4pyGaagnX2duistDOIaVsD0,40969
40
44
  glaip_sdk/utils/rendering/renderer/config.py,sha256=E4ER8TJJbqr1hcWjkwG7XROqLuccQy4EL99CbuLvSXE,783
41
45
  glaip_sdk/utils/rendering/renderer/console.py,sha256=4cLOw4Q1fkHkApuj6dWW8eYpeYdcT0t2SO5MbVt5UTc,1844
42
46
  glaip_sdk/utils/rendering/renderer/debug.py,sha256=FEYxAu4ZB0CjrJKevqQ2TKDgElA2cf6GqZXCNm12sNQ,3721
@@ -47,9 +51,9 @@ glaip_sdk/utils/rendering/steps.py,sha256=4zdeyKxMbUzCal4-yv8yf18144cs5wwXaxhe6m
47
51
  glaip_sdk/utils/resource_refs.py,sha256=0YzblJNfRhz9xhpaKE9aE68XEV-6_ssr0fIkiMVOka0,5489
48
52
  glaip_sdk/utils/rich_utils.py,sha256=-Ij-1bIJvnVAi6DrfftchIlMcvOTjVmSE0Qqax0EY_s,763
49
53
  glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
50
- glaip_sdk/utils/serialization.py,sha256=DAOxi42xgE3EFCcdMLxPhQblL3uWVhndJXA2Jc-UF-I,8051
54
+ glaip_sdk/utils/serialization.py,sha256=cUE6PxqTsfJuEEmsk_Li3QmaDavTIPotEA-BQ-v5exY,9043
51
55
  glaip_sdk/utils/validation.py,sha256=QNORcdyvuliEs4EH2_mkDgmoyT9utgl7YNhaf45SEf8,6992
52
- glaip_sdk-0.0.7.dist-info/METADATA,sha256=2NLkepPjdP_WEon0dKF2IZGi3Co6a4rzPznhpP_HAUY,4942
53
- glaip_sdk-0.0.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
54
- glaip_sdk-0.0.7.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
55
- glaip_sdk-0.0.7.dist-info/RECORD,,
56
+ glaip_sdk-0.0.8.dist-info/METADATA,sha256=yWuZjBH9LvHlgsGZ8NP8GsUpsAM2X2ScE9uNCDnRPls,4942
57
+ glaip_sdk-0.0.8.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
58
+ glaip_sdk-0.0.8.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
59
+ glaip_sdk-0.0.8.dist-info/RECORD,,