glaip-sdk 0.0.0b99__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.
Files changed (207) hide show
  1. glaip_sdk/__init__.py +52 -0
  2. glaip_sdk/_version.py +81 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1227 -0
  5. glaip_sdk/branding.py +211 -0
  6. glaip_sdk/cli/__init__.py +9 -0
  7. glaip_sdk/cli/account_store.py +540 -0
  8. glaip_sdk/cli/agent_config.py +78 -0
  9. glaip_sdk/cli/auth.py +705 -0
  10. glaip_sdk/cli/commands/__init__.py +5 -0
  11. glaip_sdk/cli/commands/accounts.py +746 -0
  12. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  13. glaip_sdk/cli/commands/agents/_common.py +561 -0
  14. glaip_sdk/cli/commands/agents/create.py +151 -0
  15. glaip_sdk/cli/commands/agents/delete.py +64 -0
  16. glaip_sdk/cli/commands/agents/get.py +89 -0
  17. glaip_sdk/cli/commands/agents/list.py +129 -0
  18. glaip_sdk/cli/commands/agents/run.py +264 -0
  19. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  20. glaip_sdk/cli/commands/agents/update.py +112 -0
  21. glaip_sdk/cli/commands/common_config.py +104 -0
  22. glaip_sdk/cli/commands/configure.py +895 -0
  23. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  24. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  25. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  26. glaip_sdk/cli/commands/mcps/create.py +152 -0
  27. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  28. glaip_sdk/cli/commands/mcps/get.py +212 -0
  29. glaip_sdk/cli/commands/mcps/list.py +69 -0
  30. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  31. glaip_sdk/cli/commands/mcps/update.py +190 -0
  32. glaip_sdk/cli/commands/models.py +67 -0
  33. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  34. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  35. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  36. glaip_sdk/cli/commands/tools/_common.py +80 -0
  37. glaip_sdk/cli/commands/tools/create.py +228 -0
  38. glaip_sdk/cli/commands/tools/delete.py +61 -0
  39. glaip_sdk/cli/commands/tools/get.py +103 -0
  40. glaip_sdk/cli/commands/tools/list.py +69 -0
  41. glaip_sdk/cli/commands/tools/script.py +49 -0
  42. glaip_sdk/cli/commands/tools/update.py +102 -0
  43. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  44. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  45. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  46. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  47. glaip_sdk/cli/commands/transcripts_original.py +756 -0
  48. glaip_sdk/cli/commands/update.py +192 -0
  49. glaip_sdk/cli/config.py +95 -0
  50. glaip_sdk/cli/constants.py +38 -0
  51. glaip_sdk/cli/context.py +150 -0
  52. glaip_sdk/cli/core/__init__.py +79 -0
  53. glaip_sdk/cli/core/context.py +124 -0
  54. glaip_sdk/cli/core/output.py +851 -0
  55. glaip_sdk/cli/core/prompting.py +649 -0
  56. glaip_sdk/cli/core/rendering.py +187 -0
  57. glaip_sdk/cli/display.py +355 -0
  58. glaip_sdk/cli/hints.py +57 -0
  59. glaip_sdk/cli/io.py +112 -0
  60. glaip_sdk/cli/main.py +686 -0
  61. glaip_sdk/cli/masking.py +136 -0
  62. glaip_sdk/cli/mcp_validators.py +287 -0
  63. glaip_sdk/cli/pager.py +266 -0
  64. glaip_sdk/cli/parsers/__init__.py +7 -0
  65. glaip_sdk/cli/parsers/json_input.py +177 -0
  66. glaip_sdk/cli/resolution.py +68 -0
  67. glaip_sdk/cli/rich_helpers.py +27 -0
  68. glaip_sdk/cli/slash/__init__.py +15 -0
  69. glaip_sdk/cli/slash/accounts_controller.py +580 -0
  70. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  71. glaip_sdk/cli/slash/agent_session.py +285 -0
  72. glaip_sdk/cli/slash/prompt.py +256 -0
  73. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  74. glaip_sdk/cli/slash/session.py +1724 -0
  75. glaip_sdk/cli/slash/tui/__init__.py +34 -0
  76. glaip_sdk/cli/slash/tui/accounts.tcss +88 -0
  77. glaip_sdk/cli/slash/tui/accounts_app.py +933 -0
  78. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  79. glaip_sdk/cli/slash/tui/clipboard.py +147 -0
  80. glaip_sdk/cli/slash/tui/context.py +59 -0
  81. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  82. glaip_sdk/cli/slash/tui/loading.py +58 -0
  83. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  84. glaip_sdk/cli/slash/tui/terminal.py +402 -0
  85. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  86. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  87. glaip_sdk/cli/slash/tui/theme/manager.py +86 -0
  88. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  89. glaip_sdk/cli/slash/tui/toast.py +123 -0
  90. glaip_sdk/cli/transcript/__init__.py +31 -0
  91. glaip_sdk/cli/transcript/cache.py +536 -0
  92. glaip_sdk/cli/transcript/capture.py +329 -0
  93. glaip_sdk/cli/transcript/export.py +38 -0
  94. glaip_sdk/cli/transcript/history.py +815 -0
  95. glaip_sdk/cli/transcript/launcher.py +77 -0
  96. glaip_sdk/cli/transcript/viewer.py +374 -0
  97. glaip_sdk/cli/update_notifier.py +369 -0
  98. glaip_sdk/cli/validators.py +238 -0
  99. glaip_sdk/client/__init__.py +12 -0
  100. glaip_sdk/client/_schedule_payloads.py +89 -0
  101. glaip_sdk/client/agent_runs.py +147 -0
  102. glaip_sdk/client/agents.py +1353 -0
  103. glaip_sdk/client/base.py +502 -0
  104. glaip_sdk/client/main.py +253 -0
  105. glaip_sdk/client/mcps.py +401 -0
  106. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  107. glaip_sdk/client/payloads/agent/requests.py +495 -0
  108. glaip_sdk/client/payloads/agent/responses.py +43 -0
  109. glaip_sdk/client/run_rendering.py +747 -0
  110. glaip_sdk/client/schedules.py +439 -0
  111. glaip_sdk/client/shared.py +21 -0
  112. glaip_sdk/client/tools.py +690 -0
  113. glaip_sdk/client/validators.py +198 -0
  114. glaip_sdk/config/constants.py +52 -0
  115. glaip_sdk/exceptions.py +113 -0
  116. glaip_sdk/hitl/__init__.py +15 -0
  117. glaip_sdk/hitl/local.py +151 -0
  118. glaip_sdk/icons.py +25 -0
  119. glaip_sdk/mcps/__init__.py +21 -0
  120. glaip_sdk/mcps/base.py +345 -0
  121. glaip_sdk/models/__init__.py +107 -0
  122. glaip_sdk/models/agent.py +47 -0
  123. glaip_sdk/models/agent_runs.py +117 -0
  124. glaip_sdk/models/common.py +42 -0
  125. glaip_sdk/models/mcp.py +33 -0
  126. glaip_sdk/models/schedule.py +224 -0
  127. glaip_sdk/models/tool.py +33 -0
  128. glaip_sdk/payload_schemas/__init__.py +7 -0
  129. glaip_sdk/payload_schemas/agent.py +85 -0
  130. glaip_sdk/registry/__init__.py +55 -0
  131. glaip_sdk/registry/agent.py +164 -0
  132. glaip_sdk/registry/base.py +139 -0
  133. glaip_sdk/registry/mcp.py +253 -0
  134. glaip_sdk/registry/tool.py +393 -0
  135. glaip_sdk/rich_components.py +125 -0
  136. glaip_sdk/runner/__init__.py +59 -0
  137. glaip_sdk/runner/base.py +84 -0
  138. glaip_sdk/runner/deps.py +112 -0
  139. glaip_sdk/runner/langgraph.py +870 -0
  140. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  141. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  142. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  143. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  144. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  145. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  146. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  147. glaip_sdk/schedules/__init__.py +22 -0
  148. glaip_sdk/schedules/base.py +291 -0
  149. glaip_sdk/tools/__init__.py +22 -0
  150. glaip_sdk/tools/base.py +466 -0
  151. glaip_sdk/utils/__init__.py +86 -0
  152. glaip_sdk/utils/a2a/__init__.py +34 -0
  153. glaip_sdk/utils/a2a/event_processor.py +188 -0
  154. glaip_sdk/utils/agent_config.py +194 -0
  155. glaip_sdk/utils/bundler.py +267 -0
  156. glaip_sdk/utils/client.py +111 -0
  157. glaip_sdk/utils/client_utils.py +486 -0
  158. glaip_sdk/utils/datetime_helpers.py +58 -0
  159. glaip_sdk/utils/discovery.py +78 -0
  160. glaip_sdk/utils/display.py +135 -0
  161. glaip_sdk/utils/export.py +143 -0
  162. glaip_sdk/utils/general.py +61 -0
  163. glaip_sdk/utils/import_export.py +168 -0
  164. glaip_sdk/utils/import_resolver.py +530 -0
  165. glaip_sdk/utils/instructions.py +101 -0
  166. glaip_sdk/utils/rendering/__init__.py +115 -0
  167. glaip_sdk/utils/rendering/formatting.py +264 -0
  168. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  169. glaip_sdk/utils/rendering/layout/panels.py +156 -0
  170. glaip_sdk/utils/rendering/layout/progress.py +202 -0
  171. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  172. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  173. glaip_sdk/utils/rendering/models.py +85 -0
  174. glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
  175. glaip_sdk/utils/rendering/renderer/base.py +1082 -0
  176. glaip_sdk/utils/rendering/renderer/config.py +27 -0
  177. glaip_sdk/utils/rendering/renderer/console.py +55 -0
  178. glaip_sdk/utils/rendering/renderer/debug.py +178 -0
  179. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  180. glaip_sdk/utils/rendering/renderer/stream.py +202 -0
  181. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  182. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  183. glaip_sdk/utils/rendering/renderer/toggle.py +182 -0
  184. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  185. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  186. glaip_sdk/utils/rendering/state.py +204 -0
  187. glaip_sdk/utils/rendering/step_tree_state.py +100 -0
  188. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  189. glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
  190. glaip_sdk/utils/rendering/steps/format.py +176 -0
  191. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  192. glaip_sdk/utils/rendering/timing.py +36 -0
  193. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  194. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  195. glaip_sdk/utils/resource_refs.py +195 -0
  196. glaip_sdk/utils/run_renderer.py +41 -0
  197. glaip_sdk/utils/runtime_config.py +425 -0
  198. glaip_sdk/utils/serialization.py +424 -0
  199. glaip_sdk/utils/sync.py +142 -0
  200. glaip_sdk/utils/tool_detection.py +33 -0
  201. glaip_sdk/utils/tool_storage_provider.py +140 -0
  202. glaip_sdk/utils/validation.py +264 -0
  203. glaip_sdk-0.0.0b99.dist-info/METADATA +239 -0
  204. glaip_sdk-0.0.0b99.dist-info/RECORD +207 -0
  205. glaip_sdk-0.0.0b99.dist-info/WHEEL +5 -0
  206. glaip_sdk-0.0.0b99.dist-info/entry_points.txt +2 -0
  207. glaip_sdk-0.0.0b99.dist-info/top_level.txt +1 -0
@@ -0,0 +1,369 @@
1
+ """Utility helpers for checking and displaying SDK update notifications.
2
+
3
+ Author:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ import logging
11
+ import sys
12
+ from collections.abc import Callable, Iterable, Iterator
13
+ from contextlib import contextmanager
14
+ from typing import Any, Literal
15
+
16
+ import click
17
+ import httpx
18
+ from packaging.version import InvalidVersion, Version
19
+ from rich import box
20
+ from rich.console import Console
21
+
22
+ from glaip_sdk.branding import (
23
+ ACCENT_STYLE,
24
+ ERROR_STYLE,
25
+ INFO_STYLE,
26
+ SUCCESS_STYLE,
27
+ WARNING_STYLE,
28
+ )
29
+ from glaip_sdk.cli.commands.update import (
30
+ PACKAGE_NAME,
31
+ _build_command_parts,
32
+ _build_manual_upgrade_command,
33
+ _is_uv_managed_environment,
34
+ update_command,
35
+ )
36
+ from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
37
+ from glaip_sdk.cli.hints import command_hint, format_command_hint
38
+ from glaip_sdk.rich_components import AIPPanel
39
+
40
+ FetchLatestVersion = Callable[[], str | None]
41
+
42
+ PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
43
+ DEFAULT_TIMEOUT = 1.5 # seconds
44
+
45
+ _LOGGER = logging.getLogger(__name__)
46
+
47
+
48
+ def _parse_version(value: str) -> Version | None:
49
+ """Parse a version string into a `Version`, returning None on failure."""
50
+ try:
51
+ return Version(value)
52
+ except InvalidVersion:
53
+ return None
54
+
55
+
56
+ def _fetch_latest_version(package_name: str) -> str | None:
57
+ """Fetch the latest published version from PyPI."""
58
+ url = PYPI_JSON_URL.format(package=package_name)
59
+ timeout = httpx.Timeout(DEFAULT_TIMEOUT)
60
+
61
+ try:
62
+ with _suppress_library_logging():
63
+ with httpx.Client(timeout=timeout) as client:
64
+ response = client.get(url, headers={"Accept": "application/json"})
65
+ response.raise_for_status()
66
+ payload = response.json()
67
+ except httpx.HTTPError as exc:
68
+ _LOGGER.debug("Update check failed: %s", exc, exc_info=True)
69
+ return None
70
+ except ValueError as exc:
71
+ _LOGGER.debug("Invalid JSON while checking for updates: %s", exc, exc_info=True)
72
+ return None
73
+
74
+ info = payload.get("info") if isinstance(payload, dict) else None
75
+ latest_version = info.get("version") if isinstance(info, dict) else None
76
+ if isinstance(latest_version, str) and latest_version.strip():
77
+ return latest_version.strip()
78
+ return None
79
+
80
+
81
+ def _should_check_for_updates() -> bool:
82
+ """Return False when update checks are explicitly disabled."""
83
+ # Check module attribute first (for test overrides), then fall back to imported constant
84
+ module = sys.modules.get(__name__)
85
+ if module and hasattr(module, "UPDATE_CHECK_ENABLED"):
86
+ return getattr(module, "UPDATE_CHECK_ENABLED")
87
+ return UPDATE_CHECK_ENABLED
88
+
89
+
90
+ def _build_update_panel(
91
+ current_version: str,
92
+ latest_version: str,
93
+ command_text: str,
94
+ *,
95
+ show_command_hint: bool,
96
+ ) -> AIPPanel:
97
+ """Create a Rich panel that prompts the user to update."""
98
+ command_markup = format_command_hint(command_text) or command_text
99
+ message = (
100
+ f"[{WARNING_STYLE}]✨ Update available![/] "
101
+ f"{current_version} → {latest_version}\n\n"
102
+ "See the latest release notes:\n"
103
+ f"https://pypi.org/project/glaip-sdk/{latest_version}/"
104
+ )
105
+ if show_command_hint:
106
+ message += f"\n\n[{ACCENT_STYLE}]Run[/] {command_markup} to install."
107
+ return AIPPanel(
108
+ message,
109
+ title=f"[{SUCCESS_STYLE}]AIP SDK Update[/]",
110
+ box=box.ROUNDED,
111
+ padding=(0, 3),
112
+ expand=False,
113
+ )
114
+
115
+
116
+ def maybe_notify_update(
117
+ current_version: str,
118
+ *,
119
+ package_name: str = "glaip-sdk",
120
+ console: Console | None = None,
121
+ fetch_latest_version: FetchLatestVersion | None = None,
122
+ ctx: Any | None = None,
123
+ slash_command: str | None = None,
124
+ style: Literal["panel", "inline"] = "panel",
125
+ ) -> None:
126
+ """Check PyPI for a newer version and display a prompt if one exists."""
127
+ if not _should_check_for_updates():
128
+ return
129
+
130
+ fetcher = fetch_latest_version or (lambda: _fetch_latest_version(package_name))
131
+ latest_version = fetcher()
132
+ if not latest_version:
133
+ return
134
+
135
+ current = _parse_version(current_version)
136
+ latest = _parse_version(latest_version)
137
+ if current is None or latest is None or latest <= current:
138
+ return
139
+
140
+ command_text = command_hint("update", slash_command=slash_command, ctx=ctx)
141
+ if command_text is None:
142
+ return
143
+
144
+ active_console = console or Console()
145
+ should_prompt = _should_prompt_for_action(active_console, ctx)
146
+
147
+ if style == "inline":
148
+ if should_prompt:
149
+ message = (
150
+ f"[{WARNING_STYLE}]✨ Update[/] "
151
+ f"{current_version} → {latest_version} "
152
+ "- choose Update now or Skip to continue."
153
+ )
154
+ active_console.print(message)
155
+ _handle_update_decision(active_console, ctx)
156
+ return
157
+
158
+ command_markup = format_command_hint(command_text) or command_text
159
+ active_console.print(f"[{WARNING_STYLE}]✨ Update[/] {current_version} → {latest_version} - {command_markup}")
160
+ return
161
+
162
+ panel = _build_update_panel(
163
+ current_version,
164
+ latest_version,
165
+ command_text,
166
+ show_command_hint=not should_prompt,
167
+ )
168
+ active_console.print(panel)
169
+ if should_prompt:
170
+ _handle_update_decision(active_console, ctx)
171
+
172
+
173
+ def _handle_update_decision(console: Console, ctx: Any) -> None:
174
+ """Prompt the user to take action on the available update."""
175
+ choice = _prompt_update_decision(console)
176
+ if choice == "skip":
177
+ return
178
+
179
+ _run_update_command(console, ctx)
180
+
181
+
182
+ def _should_prompt_for_action(console: Console, ctx: Any | None) -> bool:
183
+ """Return True when we can safely block for interactive input."""
184
+ if ctx is None or not hasattr(ctx, "invoke"):
185
+ return False
186
+
187
+ is_interactive = getattr(console, "is_interactive", False)
188
+ if not isinstance(is_interactive, bool) or not is_interactive:
189
+ return False
190
+
191
+ is_terminal = getattr(console, "is_terminal", False)
192
+ if not isinstance(is_terminal, bool) or not is_terminal:
193
+ return False
194
+
195
+ input_method = getattr(console, "input", None)
196
+ return callable(input_method)
197
+
198
+
199
+ def _prompt_update_decision(console: Console) -> Literal["update", "skip"]:
200
+ """Ask the user to choose between updating now or skipping."""
201
+ console.print(
202
+ f"[{ACCENT_STYLE}]Select an option to continue:[/]\n"
203
+ f" [{SUCCESS_STYLE}]1.[/] Update now\n"
204
+ f" [{WARNING_STYLE}]2.[/] Skip\n"
205
+ )
206
+ console.print("[dim]Press Enter after typing your choice.[/]")
207
+
208
+ while True:
209
+ try:
210
+ response = console.input("Choice [1/2]: ").strip().lower()
211
+ except (KeyboardInterrupt, EOFError):
212
+ console.print(f"\n[{WARNING_STYLE}]Update skipped.[/]")
213
+ return "skip"
214
+
215
+ if response in {"1", "update", "u"}:
216
+ return "update"
217
+ if response in {"2", "skip", "s"}:
218
+ return "skip"
219
+
220
+ console.print(f"[{ERROR_STYLE}]Please enter 1 to update now or 2 to skip.[/]")
221
+
222
+
223
+ def _get_manual_upgrade_command(is_uv: bool) -> str:
224
+ """Get the manual upgrade command for the given environment type.
225
+
226
+ Args:
227
+ is_uv: True if running in uv tool environment, False for pip environment.
228
+
229
+ Returns:
230
+ Manual upgrade command string.
231
+ """
232
+ try:
233
+ return _build_manual_upgrade_command(include_prerelease=False, is_uv=is_uv)
234
+ except Exception:
235
+ # Fallback: rebuild from shared command parts to avoid hardcoded strings.
236
+ try:
237
+ command_parts, _ = _build_command_parts(
238
+ package_name=PACKAGE_NAME,
239
+ is_uv=is_uv,
240
+ force_reinstall=False,
241
+ include_prerelease=False,
242
+ )
243
+ except Exception:
244
+ command_parts = (
245
+ ["uv", "tool", "install", "--upgrade", PACKAGE_NAME]
246
+ if is_uv
247
+ else ["pip", "install", "--upgrade", PACKAGE_NAME]
248
+ )
249
+ return " ".join(command_parts)
250
+
251
+
252
+ def _show_proactive_uv_guidance(console: Console, is_uv: bool) -> None:
253
+ """Show proactive guidance for uv environments before update attempt.
254
+
255
+ Args:
256
+ console: Rich console for output.
257
+ is_uv: True if running in uv tool environment.
258
+ """
259
+ if not is_uv:
260
+ return
261
+
262
+ manual_cmd = _get_manual_upgrade_command(is_uv=True)
263
+ console.print(
264
+ f"[{INFO_STYLE}]💡 Detected uv tool environment.[/] "
265
+ f"If automatic update fails, run: [{ACCENT_STYLE}]{manual_cmd}[/]"
266
+ )
267
+
268
+
269
+ def _show_error_guidance(console: Console, is_uv: bool) -> None:
270
+ """Show error guidance with correct manual command based on environment.
271
+
272
+ Args:
273
+ console: Rich console for output.
274
+ is_uv: True if running in uv tool environment.
275
+ """
276
+ try:
277
+ manual_cmd = _get_manual_upgrade_command(is_uv=is_uv)
278
+ console.print(f"[{INFO_STYLE}]💡 Tip:[/] Run this command manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
279
+ except Exception as exc: # pragma: no cover - defensive guard
280
+ _LOGGER.debug("Failed to render update tip: %s", exc, exc_info=True)
281
+
282
+
283
+ def _run_update_command(console: Console, ctx: Any) -> None:
284
+ """Invoke the built-in update command and surface any errors."""
285
+ # Detect uv environment proactively before attempting update
286
+ is_uv = _is_uv_managed_environment()
287
+
288
+ # Provide proactive guidance for uv environments
289
+ # This helps users on older versions (e.g., 0.6.19) that don't have uv detection
290
+ # in their update command
291
+ _show_proactive_uv_guidance(console, is_uv)
292
+
293
+ try:
294
+ ctx.invoke(update_command)
295
+ except click.ClickException as exc:
296
+ exc.show()
297
+ console.print(f"[{ERROR_STYLE}]Update command exited with an error.[/]")
298
+ _show_error_guidance(console, is_uv)
299
+ except click.Abort:
300
+ console.print(f"[{WARNING_STYLE}]Update aborted by user.[/]")
301
+ except Exception as exc: # pragma: no cover - defensive guard
302
+ console.print(f"[{ERROR_STYLE}]Unexpected error while running update: {exc}[/]")
303
+ # Also provide guidance for unexpected errors in uv environments
304
+ if is_uv:
305
+ manual_cmd = _get_manual_upgrade_command(is_uv=True)
306
+ console.print(f"[{INFO_STYLE}]💡 Tip:[/] Try running manually:\n [{ACCENT_STYLE}]{manual_cmd}[/]")
307
+ else:
308
+ _refresh_installed_version(console, ctx)
309
+
310
+
311
+ @contextmanager
312
+ def _suppress_library_logging(
313
+ logger_names: Iterable[str] | None = None, *, level: int = logging.WARNING
314
+ ) -> Iterator[None]:
315
+ """Temporarily raise log level for selected libraries during update checks."""
316
+ names = tuple(logger_names) if logger_names is not None else ("httpx",)
317
+ captured: list[tuple[logging.Logger, int]] = []
318
+ try:
319
+ for name in names:
320
+ logger = logging.getLogger(name)
321
+ captured.append((logger, logger.level))
322
+ logger.setLevel(level)
323
+ yield
324
+ finally:
325
+ for logger, previous_level in captured:
326
+ logger.setLevel(previous_level)
327
+
328
+
329
+ def _refresh_installed_version(console: Console, ctx: Any) -> None:
330
+ """Reload runtime metadata after an in-process upgrade."""
331
+ new_version: str | None = None
332
+ branding_module: Any | None = None
333
+
334
+ try:
335
+ version_module = importlib.reload(importlib.import_module("glaip_sdk._version"))
336
+ new_version = getattr(version_module, "__version__", None)
337
+ except Exception as exc: # pragma: no cover - defensive guard
338
+ _LOGGER.debug("Failed to reload glaip_sdk._version: %s", exc, exc_info=True)
339
+
340
+ try:
341
+ branding_module = importlib.reload(importlib.import_module("glaip_sdk.branding"))
342
+ if new_version:
343
+ branding_module.SDK_VERSION = new_version
344
+ except Exception as exc: # pragma: no cover - defensive guard
345
+ _LOGGER.debug("Failed to update branding metadata: %s", exc, exc_info=True)
346
+ branding_module = None
347
+
348
+ session = _get_slash_session(ctx)
349
+ if session and hasattr(session, "refresh_branding"):
350
+ try:
351
+ branding_cls = getattr(branding_module, "AIPBranding", None) if branding_module else None
352
+ session.refresh_branding(new_version, branding_cls=branding_cls)
353
+ return
354
+ except Exception as exc: # pragma: no cover - defensive guard
355
+ _LOGGER.debug("Failed to refresh active slash session: %s", exc, exc_info=True)
356
+
357
+ if new_version:
358
+ console.print(f"[{SUCCESS_STYLE}]CLI now running glaip-sdk {new_version}.[/]")
359
+
360
+
361
+ def _get_slash_session(ctx: Any) -> Any | None:
362
+ """Return active slash session from the Click context if present."""
363
+ ctx_obj = getattr(ctx, "obj", None)
364
+ if isinstance(ctx_obj, dict):
365
+ return ctx_obj.get("_slash_session")
366
+ return None
367
+
368
+
369
+ __all__ = ["maybe_notify_update"]
@@ -0,0 +1,238 @@
1
+ """CLI validation utilities that wrap core validation with Click exceptions.
2
+
3
+ This module provides thin wrappers over utils.validation that translate
4
+ ValueError exceptions to click.ClickException for CLI user experience.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import click
15
+
16
+ from glaip_sdk.cli.core.context import handle_best_effort_check
17
+ from glaip_sdk.utils.validation import (
18
+ coerce_timeout,
19
+ validate_agent_instruction,
20
+ validate_agent_name,
21
+ validate_api_key,
22
+ validate_directory_path,
23
+ validate_file_path,
24
+ validate_mcp_name,
25
+ validate_timeout,
26
+ validate_tool_name,
27
+ validate_url,
28
+ )
29
+
30
+
31
+ def validate_agent_name_cli(name: str) -> str:
32
+ """Validate agent name and return cleaned version.
33
+
34
+ Args:
35
+ name: Agent name to validate
36
+
37
+ Returns:
38
+ Cleaned agent name
39
+
40
+ Raises:
41
+ click.ClickException: If name is invalid
42
+ """
43
+ try:
44
+ return validate_agent_name(name)
45
+ except ValueError as e:
46
+ raise click.ClickException(str(e)) from e
47
+
48
+
49
+ def validate_agent_instruction_cli(instruction: str) -> str:
50
+ """Validate agent instruction and return cleaned version.
51
+
52
+ Args:
53
+ instruction: Agent instruction to validate
54
+
55
+ Returns:
56
+ Cleaned agent instruction
57
+
58
+ Raises:
59
+ click.ClickException: If instruction is invalid
60
+ """
61
+ try:
62
+ return validate_agent_instruction(instruction)
63
+ except ValueError as e:
64
+ raise click.ClickException(str(e)) from e
65
+
66
+
67
+ def validate_timeout_cli(timeout: int) -> int:
68
+ """Validate timeout value.
69
+
70
+ Args:
71
+ timeout: Timeout value in seconds
72
+
73
+ Returns:
74
+ Validated timeout value
75
+
76
+ Raises:
77
+ click.ClickException: If timeout is invalid
78
+ """
79
+ try:
80
+ return validate_timeout(timeout)
81
+ except ValueError as e:
82
+ raise click.ClickException(str(e)) from e
83
+
84
+
85
+ def validate_tool_name_cli(name: str) -> str:
86
+ """Validate tool name and return cleaned version.
87
+
88
+ Args:
89
+ name: Tool name to validate
90
+
91
+ Returns:
92
+ Cleaned tool name
93
+
94
+ Raises:
95
+ click.ClickException: If name is invalid
96
+ """
97
+ try:
98
+ return validate_tool_name(name)
99
+ except ValueError as e:
100
+ raise click.ClickException(str(e)) from e
101
+
102
+
103
+ def validate_mcp_name_cli(name: str) -> str:
104
+ """Validate MCP name and return cleaned version.
105
+
106
+ Args:
107
+ name: MCP name to validate
108
+
109
+ Returns:
110
+ Cleaned MCP name
111
+
112
+ Raises:
113
+ click.ClickException: If name is invalid
114
+ """
115
+ try:
116
+ return validate_mcp_name(name)
117
+ except ValueError as e:
118
+ raise click.ClickException(str(e)) from e
119
+
120
+
121
+ def validate_file_path_cli(file_path: str | Path, must_exist: bool = True) -> Path:
122
+ """Validate file path.
123
+
124
+ Args:
125
+ file_path: File path to validate
126
+ must_exist: Whether file must exist
127
+
128
+ Returns:
129
+ Path object
130
+
131
+ Raises:
132
+ click.ClickException: If file path is invalid
133
+ """
134
+ try:
135
+ return validate_file_path(file_path, must_exist)
136
+ except ValueError as e:
137
+ raise click.ClickException(str(e)) from e
138
+
139
+
140
+ def validate_directory_path_cli(dir_path: str | Path, must_exist: bool = True) -> Path:
141
+ """Validate directory path.
142
+
143
+ Args:
144
+ dir_path: Directory path to validate
145
+ must_exist: Whether directory must exist
146
+
147
+ Returns:
148
+ Path object
149
+
150
+ Raises:
151
+ click.ClickException: If directory path is invalid
152
+ """
153
+ try:
154
+ return validate_directory_path(dir_path, must_exist)
155
+ except ValueError as e:
156
+ raise click.ClickException(str(e)) from e
157
+
158
+
159
+ def validate_url_cli(url: str) -> str:
160
+ """Validate URL format.
161
+
162
+ Args:
163
+ url: URL to validate
164
+
165
+ Returns:
166
+ Validated URL
167
+
168
+ Raises:
169
+ click.ClickException: If URL is invalid
170
+ """
171
+ try:
172
+ return validate_url(url)
173
+ except ValueError as e:
174
+ raise click.ClickException(str(e)) from e
175
+
176
+
177
+ def validate_api_key_cli(api_key: str) -> str:
178
+ """Validate API key format.
179
+
180
+ Args:
181
+ api_key: API key to validate
182
+
183
+ Returns:
184
+ Validated API key
185
+
186
+ Raises:
187
+ click.ClickException: If API key is invalid
188
+ """
189
+ try:
190
+ return validate_api_key(api_key)
191
+ except ValueError as e:
192
+ raise click.ClickException(str(e)) from e
193
+
194
+
195
+ def coerce_timeout_cli(value: int | float | str) -> int:
196
+ """Coerce timeout value to integer with CLI-friendly error handling.
197
+
198
+ Args:
199
+ value: The timeout value to coerce (int, float, str, etc.)
200
+
201
+ Returns:
202
+ Integer timeout value
203
+
204
+ Raises:
205
+ click.ClickException: If value cannot be coerced to valid timeout
206
+ """
207
+ try:
208
+ return coerce_timeout(value)
209
+ except ValueError as e:
210
+ raise click.ClickException(str(e)) from e
211
+
212
+
213
+ def validate_name_uniqueness_cli(
214
+ _client: Any,
215
+ name: str,
216
+ resource_type: str,
217
+ finder_func: Callable[..., list[Any]],
218
+ ) -> None:
219
+ """Validate that a resource name is unique.
220
+
221
+ Args:
222
+ client: API client
223
+ name: Name to validate
224
+ resource_type: Type of resource (for error messages)
225
+ finder_func: Function to find existing resources by name
226
+
227
+ Raises:
228
+ click.ClickException: If name is not unique
229
+ """
230
+
231
+ def _check_duplicate() -> None:
232
+ existing = finder_func(name=name)
233
+ if existing:
234
+ raise click.ClickException(
235
+ f"A {resource_type.lower()} named '{name}' already exists. Please choose a unique name."
236
+ )
237
+
238
+ handle_best_effort_check(_check_duplicate)
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env python3
2
+ """Client module for AIP SDK.
3
+
4
+ Authors:
5
+ Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ """
7
+
8
+ from glaip_sdk.client.agent_runs import AgentRunsClient
9
+ from glaip_sdk.client.main import Client
10
+ from glaip_sdk.client.schedules import AgentScheduleManager, ScheduleClient
11
+
12
+ __all__ = ["AgentRunsClient", "AgentScheduleManager", "Client", "ScheduleClient"]
@@ -0,0 +1,89 @@
1
+ """Schedule request payload builders for AIP SDK.
2
+
3
+ Authors:
4
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ from glaip_sdk.models.schedule import ScheduleConfig
11
+
12
+
13
+ @dataclass
14
+ class ScheduleListParams:
15
+ """Parameters for listing schedules.
16
+
17
+ Args:
18
+ limit: Maximum number of schedules to return (1-100, default 50)
19
+ page: Page number for pagination (default 1)
20
+ agent_id: Filter schedules by agent ID
21
+ """
22
+
23
+ limit: int | None = None
24
+ page: int | None = None
25
+ agent_id: str | None = None
26
+
27
+ def to_query_params(self) -> dict[str, Any]:
28
+ """Convert to query parameters dictionary.
29
+
30
+ Returns:
31
+ Dictionary of non-None parameters for the API request
32
+ """
33
+ params: dict[str, Any] = {}
34
+ if self.limit is not None:
35
+ params["limit"] = self.limit
36
+ if self.page is not None:
37
+ params["page"] = self.page
38
+ if self.agent_id is not None:
39
+ params["agent_id"] = self.agent_id
40
+ return params
41
+
42
+
43
+ def normalize_schedule(
44
+ schedule: ScheduleConfig | dict[str, str] | str | None,
45
+ ) -> dict[str, str] | None:
46
+ """Normalize schedule input to a dictionary for API requests.
47
+
48
+ Accepts multiple input formats for user convenience:
49
+ - ScheduleConfig: Pydantic model with cron fields
50
+ - dict: Dictionary with cron fields (minute, hour, etc.)
51
+ - str: Cron string like "0 9 * * 1-5"
52
+ - None: Returns None
53
+
54
+ Args:
55
+ schedule: Schedule in various formats
56
+
57
+ Returns:
58
+ Dictionary suitable for API request or None
59
+
60
+ Raises:
61
+ ValueError: If cron string format is invalid
62
+ TypeError: If schedule is an unsupported type
63
+
64
+ Examples:
65
+ >>> normalize_schedule(ScheduleConfig(minute="0", hour="9"))
66
+ {'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '*'}
67
+
68
+ >>> normalize_schedule({"minute": "0", "hour": "9"})
69
+ {'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '*'}
70
+
71
+ >>> normalize_schedule("0 9 * * 1-5")
72
+ {'minute': '0', 'hour': '9', 'day_of_month': '*', 'month': '*', 'day_of_week': '1-5'}
73
+ """
74
+ if schedule is None:
75
+ return None
76
+
77
+ if isinstance(schedule, ScheduleConfig):
78
+ return schedule.model_dump()
79
+
80
+ if isinstance(schedule, dict):
81
+ # Validate and merge with defaults
82
+ return ScheduleConfig(**schedule).model_dump()
83
+
84
+ if isinstance(schedule, str):
85
+ # Parse cron string
86
+ config = ScheduleConfig.from_cron_string(schedule)
87
+ return config.model_dump()
88
+
89
+ raise TypeError(f"schedule must be ScheduleConfig, dict, or str, got {type(schedule).__name__}")