glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5b1__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 (48) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +18 -17
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +570 -673
  7. glaip_sdk/cli/commands/configure.py +2 -2
  8. glaip_sdk/cli/commands/mcps.py +148 -143
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +250 -179
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +14 -18
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +305 -264
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +631 -191
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +146 -11
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +42 -2
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/METADATA +22 -21
  43. glaip_sdk-0.0.5b1.dist-info/RECORD +55 -0
  44. {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5b1.dist-info}/WHEEL +1 -1
  45. glaip_sdk-0.0.5b1.dist-info/entry_points.txt +3 -0
  46. glaip_sdk/cli/commands/init.py +0 -93
  47. glaip_sdk-0.0.4.dist-info/RECORD +0 -41
  48. glaip_sdk-0.0.4.dist-info/entry_points.txt +0 -2
@@ -0,0 +1,181 @@
1
+ """Agent configuration utilities for import/export normalization and LM selection.
2
+
3
+ This module consolidates language model selection logic and agent configuration
4
+ sanitization that was previously split between CLI and SDK layers.
5
+
6
+ Authors:
7
+ Raymond Christopher (raymond.christopher@gdplabs.id)
8
+ """
9
+
10
+ from typing import Any
11
+
12
+
13
+ def _strip_agent_config_credentials(agent_config: dict | None) -> dict:
14
+ """Return agent_config without sensitive credentials; keep all other keys.
15
+
16
+ We intentionally keep keys like memory and agent_id so that backends supporting
17
+ mem0 memory (gllm-agents-binary>=0.4.6) receive them under agent_config.
18
+
19
+ Args:
20
+ agent_config: The agent configuration dictionary
21
+
22
+ Returns:
23
+ Agent config with credentials stripped but mem0 keys preserved
24
+ """
25
+ if not isinstance(agent_config, dict):
26
+ return {}
27
+ return {k: v for k, v in agent_config.items() if k != "lm_credentials"}
28
+
29
+
30
+ def sanitize_agent_config(
31
+ agent_config: dict | None,
32
+ *,
33
+ strip_credentials: bool = True,
34
+ strip_lm_identity: bool = False,
35
+ ) -> dict:
36
+ """Sanitize agent_config based on chosen LM selection method.
37
+
38
+ Args:
39
+ agent_config: The agent configuration to sanitize
40
+ strip_credentials: Always drop lm_credentials (default: True)
41
+ strip_lm_identity: Also drop lm_provider/lm_name/lm_base_url when True
42
+
43
+ Returns:
44
+ Sanitized agent configuration
45
+
46
+ Notes:
47
+ - Always drops lm_credentials to prevent credential leakage
48
+ - When strip_lm_identity=True, also drops LM identity keys to avoid conflicts
49
+ - Never drops mem0 keys (memory, agent_id) as they're needed by backends
50
+ """
51
+ if strip_credentials:
52
+ cfg = _strip_agent_config_credentials(agent_config)
53
+ else:
54
+ cfg = agent_config or {}
55
+
56
+ if strip_lm_identity and isinstance(cfg, dict):
57
+ cfg = {
58
+ k: v
59
+ for k, v in cfg.items()
60
+ if k not in {"lm_provider", "lm_name", "lm_base_url"}
61
+ }
62
+ return cfg
63
+
64
+
65
+ def resolve_language_model_selection(
66
+ merged_data: dict[str, Any], cli_model: str | None
67
+ ) -> tuple[dict[str, Any], bool]:
68
+ """Resolve language model selection from merged data and CLI args.
69
+
70
+ Implements the LM selection priority:
71
+ 1. CLI --model (maps to provider/model_name)
72
+ 2. language_model_id from import
73
+ 3. agent_config.lm_name from import (legacy)
74
+
75
+ Args:
76
+ merged_data: Merged import data and CLI args
77
+ cli_model: Model specified via CLI --model flag
78
+
79
+ Returns:
80
+ Tuple of (lm_selection_dict, should_strip_lm_identity)
81
+ - lm_selection_dict: Dict with exactly one LM method: {"language_model_id": "..."} OR {"model": "..."}
82
+ - should_strip_lm_identity: True when LM identity keys should be stripped from agent_config
83
+
84
+ Notes:
85
+ - Returns exactly one LM selection method to avoid conflicts
86
+ - CLI model takes highest priority
87
+ - language_model_id is preferred over legacy lm_name
88
+ - When extracting from agent_config, signals that LM identity should be stripped
89
+ """
90
+ # Priority 1: CLI --model flag
91
+ if cli_model:
92
+ return {"model": cli_model}, False
93
+
94
+ # Priority 2: language_model_id from import
95
+ if merged_data.get("language_model_id"):
96
+ return {"language_model_id": merged_data["language_model_id"]}, True
97
+
98
+ # Priority 3: Legacy lm_name from agent_config
99
+ agent_config = merged_data.get("agent_config") or {}
100
+ if isinstance(agent_config, dict) and agent_config.get("lm_name"):
101
+ return {
102
+ "model": agent_config["lm_name"]
103
+ }, True # Strip LM identity when extracting from agent_config
104
+
105
+ # No LM selection found
106
+ return {}, False
107
+
108
+
109
+ def normalize_agent_config_for_import(
110
+ agent_data: dict[str, Any], cli_model: str | None = None
111
+ ) -> dict[str, Any]:
112
+ """Automatically normalize agent configuration by extracting LM settings from agent_config.
113
+
114
+ This function addresses the common issue where exported agent configurations contain
115
+ language model settings in agent_config, but the backend expects them at the top level
116
+ to avoid conflicts.
117
+
118
+ Args:
119
+ agent_data: Raw agent configuration data (from import file)
120
+ cli_model: CLI model override (highest priority)
121
+
122
+ Returns:
123
+ Normalized agent configuration with LM settings properly positioned
124
+
125
+ Notes:
126
+ - Automatically extracts lm_provider/lm_name from agent_config to top level
127
+ - Preserves memory settings (memory, agent_id) in agent_config
128
+ - Handles conflicts by prioritizing CLI model > existing language_model_id > extracted lm_name
129
+ - Strips redundant LM settings from agent_config after extraction
130
+ """
131
+ normalized_data = agent_data.copy()
132
+ agent_config = normalized_data.get("agent_config", {})
133
+
134
+ if not isinstance(agent_config, dict):
135
+ return normalized_data
136
+
137
+ # Priority 1: CLI --model flag (highest priority)
138
+ if cli_model:
139
+ # When CLI model is specified, set it and don't extract from agent_config
140
+ normalized_data["model"] = cli_model
141
+ return normalized_data
142
+
143
+ # Priority 2: language_model_id already exists - clean up agent_config
144
+ if normalized_data.get("language_model_id"):
145
+ # If language_model_id exists, we should still clean up any conflicting
146
+ # LM settings from agent_config to prevent backend validation errors
147
+ if isinstance(agent_config, dict):
148
+ # Remove LM identity keys from agent_config since language_model_id takes precedence
149
+ lm_keys_to_remove = {"lm_provider", "lm_name", "lm_base_url"}
150
+ for key in lm_keys_to_remove:
151
+ agent_config.pop(key, None)
152
+ normalized_data["agent_config"] = agent_config
153
+ return normalized_data
154
+
155
+ # Priority 3: Extract LM settings from agent_config
156
+ extracted_lm = {}
157
+
158
+ # Extract lm_name if present
159
+ if "lm_name" in agent_config:
160
+ extracted_lm["model"] = agent_config["lm_name"]
161
+
162
+ # Extract lm_provider if present (for completeness)
163
+ if "lm_provider" in agent_config:
164
+ extracted_lm["lm_provider"] = agent_config["lm_provider"]
165
+
166
+ # If we extracted LM settings, update the normalized data
167
+ if extracted_lm:
168
+ # Add extracted LM settings to top level
169
+ normalized_data.update(extracted_lm)
170
+
171
+ # Create sanitized agent_config (remove extracted LM settings but keep memory)
172
+ sanitized_config = agent_config.copy()
173
+
174
+ # Remove LM identity keys but preserve memory and other settings
175
+ lm_keys_to_remove = {"lm_provider", "lm_name", "lm_base_url"}
176
+ for key in lm_keys_to_remove:
177
+ sanitized_config.pop(key, None)
178
+
179
+ normalized_data["agent_config"] = sanitized_config
180
+
181
+ return normalized_data
@@ -9,6 +9,7 @@ Authors:
9
9
  """
10
10
 
11
11
  import logging
12
+ from collections.abc import AsyncGenerator
12
13
  from contextlib import ExitStack
13
14
  from pathlib import Path
14
15
  from typing import Any, BinaryIO
@@ -42,7 +43,7 @@ class MultipartData:
42
43
  def __enter__(self):
43
44
  return self
44
45
 
45
- def __exit__(self, exc_type, exc_val, exc_tb):
46
+ def __exit__(self, _exc_type, _exc_val, _exc_tb):
46
47
  self.close()
47
48
 
48
49
 
@@ -54,21 +55,18 @@ def extract_ids(items: list[str | Any] | None) -> list[str] | None:
54
55
 
55
56
  Returns:
56
57
  List of extracted IDs, or None if items is empty/None
58
+
59
+ Note:
60
+ This function maintains backward compatibility by returning None for empty input.
61
+ New code should use glaip_sdk.utils.resource_refs.extract_ids which returns [].
57
62
  """
63
+ from .resource_refs import extract_ids as extract_ids_new
64
+
58
65
  if not items:
59
66
  return None
60
67
 
61
- ids = []
62
- for item in items:
63
- if isinstance(item, str):
64
- ids.append(item)
65
- elif hasattr(item, "id"):
66
- ids.append(item.id)
67
- else:
68
- # Fallback: convert to string
69
- ids.append(str(item))
70
-
71
- return ids
68
+ result = extract_ids_new(items)
69
+ return result if result else None
72
70
 
73
71
 
74
72
  def create_model_instances(
@@ -108,14 +106,96 @@ def find_by_name(
108
106
 
109
107
  Returns:
110
108
  Filtered list of items matching the name
109
+
110
+ Note:
111
+ This function now delegates to glaip_sdk.utils.resource_refs.find_by_name.
111
112
  """
112
- if not name:
113
- return items
113
+ from .resource_refs import find_by_name as find_by_name_new
114
+
115
+ return find_by_name_new(items, name, case_sensitive)
116
+
117
+
118
+ def _parse_sse_line(line: str, buf: list, event_type: str = None, event_id: str = None):
119
+ """Parse a single SSE line and return updated buffer and event metadata."""
120
+ # Normalize CRLF and treat whitespace-only as blank
121
+ line = line.rstrip("\r")
122
+
123
+ if not line.strip(): # blank line
124
+ if buf:
125
+ data = "\n".join(buf)
126
+ return (
127
+ [],
128
+ None,
129
+ None,
130
+ {
131
+ "event": event_type or "message",
132
+ "id": event_id,
133
+ "data": data,
134
+ },
135
+ False,
136
+ ) # no completion
137
+ return buf, event_type, event_id, None, False
138
+
139
+ if line.startswith(":"): # comment
140
+ return buf, event_type, event_id, None, False
141
+
142
+ if line.startswith("data:"):
143
+ data_line = line[5:].lstrip()
144
+ if data_line.strip() == "[DONE]": # sentinel end marker
145
+ if buf:
146
+ data = "\n".join(buf)
147
+ return (
148
+ [],
149
+ None,
150
+ None,
151
+ {
152
+ "event": event_type or "message",
153
+ "id": event_id,
154
+ "data": data,
155
+ },
156
+ True,
157
+ ) # signal completion
158
+ return buf, event_type, event_id, None, True
159
+ buf.append(data_line)
160
+ elif line.startswith("event:"):
161
+ event_type = line[6:].strip() or None
162
+ elif line.startswith("id:"):
163
+ event_id = line[3:].strip() or None
164
+
165
+ return buf, event_type, event_id, None, False
166
+
167
+
168
+ def _handle_streaming_error(
169
+ e: Exception, timeout_seconds: float = None, agent_name: str = None
170
+ ):
171
+ """Handle different types of streaming errors with appropriate logging and exceptions."""
172
+ if isinstance(e, httpx.ReadTimeout):
173
+ logger.error(f"Read timeout during streaming: {e}")
174
+ logger.error("This usually indicates the backend is taking too long to respond")
175
+ logger.error(
176
+ "Consider increasing the timeout value or checking backend performance"
177
+ )
178
+ raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
179
+
180
+ elif isinstance(e, httpx.TimeoutException):
181
+ logger.error(f"General timeout during streaming: {e}")
182
+ raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
183
+
184
+ elif isinstance(e, httpx.StreamClosed):
185
+ logger.error(f"Stream closed unexpectedly during streaming: {e}")
186
+ logger.error("This may indicate a backend issue or network problem")
187
+ logger.error("The response stream was closed before all data could be read")
188
+ raise
189
+
190
+ elif isinstance(e, httpx.ConnectError):
191
+ logger.error(f"Connection error during streaming: {e}")
192
+ logger.error("Check your network connection and backend availability")
193
+ raise
114
194
 
115
- if case_sensitive:
116
- return [item for item in items if name in item.name]
117
195
  else:
118
- return [item for item in items if name.lower() in item.name.lower()]
196
+ logger.error(f"Unexpected error during streaming: {e}")
197
+ logger.error(f"Error type: {type(e).__name__}")
198
+ raise
119
199
 
120
200
 
121
201
  def iter_sse_events(
@@ -146,41 +226,69 @@ def iter_sse_events(
146
226
  if line is None:
147
227
  continue
148
228
 
149
- # Normalize CRLF and treat whitespace-only as blank
150
- line = line.rstrip("\r")
229
+ result = _parse_sse_line(line, buf, event_type, event_id)
230
+ if len(result) == 5: # completion signal included
231
+ buf, event_type, event_id, event_data, completed = result
232
+ else: # normal case
233
+ buf, event_type, event_id, event_data = result
234
+ completed = False
151
235
 
152
- if not line.strip(): # instead of: if line == ""
153
- if buf:
154
- data = "\n".join(buf)
155
- yield {
156
- "event": event_type or "message",
157
- "id": event_id,
158
- "data": data,
159
- }
160
- buf, event_type, event_id = [], None, None
161
- continue
236
+ if event_data:
237
+ yield event_data
238
+ if completed:
239
+ return
240
+
241
+ # Flush any remaining data
242
+ if buf:
243
+ yield {
244
+ "event": event_type or "message",
245
+ "id": event_id,
246
+ "data": "\n".join(buf),
247
+ }
248
+
249
+ except Exception as e:
250
+ _handle_streaming_error(e, timeout_seconds, agent_name)
251
+
252
+
253
+ async def aiter_sse_events(
254
+ response: httpx.Response, timeout_seconds: float = None, agent_name: str = None
255
+ ) -> AsyncGenerator[dict, None]:
256
+ """Async iterate over Server-Sent Events with proper parsing.
257
+
258
+ Args:
259
+ response: HTTP response object with streaming content
260
+ timeout_seconds: Timeout duration in seconds (for error messages)
261
+ agent_name: Agent name (for error messages)
262
+
263
+ Yields:
264
+ Dictionary with event data, type, and ID
265
+
266
+ Raises:
267
+ AgentTimeoutError: When agent execution times out
268
+ httpx.TimeoutException: When general timeout occurs
269
+ Exception: For other unexpected errors
270
+ """
271
+ buf = []
272
+ event_type = None
273
+ event_id = None
162
274
 
163
- if line.startswith(":"): # comment
275
+ try:
276
+ async for raw in response.aiter_lines():
277
+ line = raw
278
+ if line is None:
164
279
  continue
165
- if line.startswith("data:"):
166
- data_line = line[5:].lstrip()
167
-
168
- # Optional: handle sentinel end markers gracefully
169
- if data_line.strip() == "[DONE]":
170
- if buf:
171
- data = "\n".join(buf)
172
- yield {
173
- "event": event_type or "message",
174
- "id": event_id,
175
- "data": data,
176
- }
177
- return
178
-
179
- buf.append(data_line)
180
- elif line.startswith("event:"):
181
- event_type = line[6:].strip() or None
182
- elif line.startswith("id:"):
183
- event_id = line[3:].strip() or None
280
+
281
+ result = _parse_sse_line(line, buf, event_type, event_id)
282
+ if len(result) == 5: # completion signal included
283
+ buf, event_type, event_id, event_data, completed = result
284
+ else: # normal case
285
+ buf, event_type, event_id, event_data = result
286
+ completed = False
287
+
288
+ if event_data:
289
+ yield event_data
290
+ if completed:
291
+ return
184
292
 
185
293
  # Flush any remaining data
186
294
  if buf:
@@ -189,37 +297,9 @@ def iter_sse_events(
189
297
  "id": event_id,
190
298
  "data": "\n".join(buf),
191
299
  }
192
- except httpx.ReadTimeout as e:
193
- logger.error(f"Read timeout during streaming: {e}")
194
- logger.error("This usually indicates the backend is taking too long to respond")
195
- logger.error(
196
- "Consider increasing the timeout value or checking backend performance"
197
- )
198
- # Raise a more user-friendly timeout error
199
- raise AgentTimeoutError(
200
- timeout_seconds or 30.0, # Default to 30s if not provided
201
- agent_name,
202
- )
203
- except httpx.TimeoutException as e:
204
- logger.error(f"General timeout during streaming: {e}")
205
- # Also convert general timeout to agent timeout for consistency
206
- raise AgentTimeoutError(timeout_seconds or 30.0, agent_name)
207
- except httpx.StreamClosed as e:
208
- logger.error(f"Stream closed unexpectedly during streaming: {e}")
209
- logger.error("This may indicate a backend issue or network problem")
210
- logger.error("The response stream was closed before all data could be read")
211
- raise
212
- except httpx.ConnectError as e:
213
- logger.error(f"Connection error during streaming: {e}")
214
- logger.error("Check your network connection and backend availability")
215
- raise
300
+
216
301
  except Exception as e:
217
- logger.error(f"Unexpected error during streaming: {e}")
218
- logger.error(f"Error type: {type(e).__name__}")
219
- # Log additional context if available
220
- if hasattr(e, "__cause__") and e.__cause__:
221
- logger.error(f"Caused by: {e.__cause__}")
222
- raise
302
+ _handle_streaming_error(e, timeout_seconds, agent_name)
223
303
 
224
304
 
225
305
  def prepare_multipart_data(message: str, files: list[str | BinaryIO]) -> MultipartData:
@@ -0,0 +1,100 @@
1
+ """Rich display utilities for enhanced output.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from glaip_sdk.utils.rich_utils import RICH_AVAILABLE
8
+
9
+
10
+ def print_agent_output(output: str, title: str = "Agent Output") -> None:
11
+ """Print agent output with rich formatting.
12
+
13
+ Args:
14
+ output: The agent's response text
15
+ title: Title for the output panel
16
+ """
17
+ if RICH_AVAILABLE:
18
+ # Lazy import Rich components
19
+ from rich.console import Console
20
+ from rich.text import Text
21
+
22
+ from glaip_sdk.rich_components import AIPPanel
23
+
24
+ console = Console()
25
+ panel = AIPPanel(
26
+ Text(output, style="green"),
27
+ title=title,
28
+ border_style="green",
29
+ )
30
+ console.print(panel)
31
+ else:
32
+ print(f"\n=== {title} ===")
33
+ print(output)
34
+ print("=" * (len(title) + 8))
35
+
36
+
37
+ def print_agent_created(agent, title: str = "🤖 Agent Created") -> None:
38
+ """Print agent creation success with rich formatting.
39
+
40
+ Args:
41
+ agent: The created agent object
42
+ title: Title for the output panel
43
+ """
44
+ if RICH_AVAILABLE:
45
+ # Lazy import Rich components
46
+ from rich.console import Console
47
+
48
+ from glaip_sdk.rich_components import AIPPanel
49
+
50
+ console = Console()
51
+ panel = AIPPanel(
52
+ f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
53
+ f"ID: {agent.id}\n"
54
+ f"Model: {getattr(agent, 'model', 'N/A')}\n"
55
+ f"Type: {getattr(agent, 'type', 'config')}\n"
56
+ f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
57
+ f"Version: {getattr(agent, 'version', '1.0')}",
58
+ title=title,
59
+ border_style="green",
60
+ )
61
+ console.print(panel)
62
+ else:
63
+ print(f"✅ Agent '{agent.name}' created successfully!")
64
+ print(f"ID: {agent.id}")
65
+ print(f"Model: {getattr(agent, 'model', 'N/A')}")
66
+ print(f"Type: {getattr(agent, 'type', 'config')}")
67
+ print(f"Framework: {getattr(agent, 'framework', 'langchain')}")
68
+ print(f"Version: {getattr(agent, 'version', '1.0')}")
69
+
70
+
71
+ def print_agent_updated(agent) -> None:
72
+ """Print agent update success with rich formatting.
73
+
74
+ Args:
75
+ agent: The updated agent object
76
+ """
77
+ if RICH_AVAILABLE:
78
+ # Lazy import Rich components
79
+ from rich.console import Console
80
+
81
+ console = Console()
82
+ console.print(f"[green]✅ Agent '{agent.name}' updated successfully[/green]")
83
+ else:
84
+ print(f"✅ Agent '{agent.name}' updated successfully")
85
+
86
+
87
+ def print_agent_deleted(agent_id: str) -> None:
88
+ """Print agent deletion success with rich formatting.
89
+
90
+ Args:
91
+ agent_id: The deleted agent's ID
92
+ """
93
+ if RICH_AVAILABLE:
94
+ # Lazy import Rich components
95
+ from rich.console import Console
96
+
97
+ console = Console()
98
+ console.print(f"[green]✅ Agent deleted successfully (ID: {agent_id})[/green]")
99
+ else:
100
+ print(f"✅ Agent deleted successfully (ID: {agent_id})")
@@ -0,0 +1,94 @@
1
+ """General utility functions for AIP SDK.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import re
8
+ from datetime import datetime
9
+ from uuid import UUID
10
+
11
+ import click
12
+
13
+
14
+ def is_uuid(value: str) -> bool:
15
+ """Check if a string is a valid UUID.
16
+
17
+ Args:
18
+ value: String to check
19
+
20
+ Returns:
21
+ True if value is a valid UUID, False otherwise
22
+ """
23
+ try:
24
+ UUID(value)
25
+ return True
26
+ except (ValueError, TypeError):
27
+ return False
28
+
29
+
30
+ def sanitize_name(name: str) -> str:
31
+ """Sanitize a name for resource creation.
32
+
33
+ Args:
34
+ name: Raw name input
35
+
36
+ Returns:
37
+ Sanitized name suitable for resource creation
38
+ """
39
+ # Remove special characters and normalize
40
+ sanitized = re.sub(r"[^a-zA-Z0-9\-_]", "-", name.strip())
41
+ sanitized = re.sub(r"-+", "-", sanitized) # Collapse multiple dashes
42
+ return sanitized.lower().strip("-")
43
+
44
+
45
+ def format_file_size(size_bytes: int) -> str:
46
+ """Format file size in human readable format.
47
+
48
+ Args:
49
+ size_bytes: Size in bytes
50
+
51
+ Returns:
52
+ Human readable size string (e.g., "1.5 MB")
53
+ """
54
+ for unit in ["B", "KB", "MB", "GB"]:
55
+ if size_bytes < 1024.0:
56
+ return f"{size_bytes:.1f} {unit}"
57
+ size_bytes /= 1024.0
58
+ return f"{size_bytes:.1f} TB"
59
+
60
+
61
+ def format_datetime(dt):
62
+ """Format datetime object to readable string.
63
+
64
+ Args:
65
+ dt: Datetime object or string to format
66
+
67
+ Returns:
68
+ Formatted datetime string or "N/A" if None
69
+ """
70
+ if isinstance(dt, datetime):
71
+ return dt.strftime("%Y-%m-%d %H:%M:%S UTC")
72
+ elif dt is None:
73
+ return "N/A"
74
+ return str(dt)
75
+
76
+
77
+ def progress_bar(iterable, description: str = "Processing"):
78
+ """Simple progress bar using click.
79
+
80
+ Args:
81
+ iterable: Iterable to process
82
+ description: Progress description
83
+
84
+ Yields:
85
+ Items from iterable with progress display
86
+ """
87
+ try:
88
+ with click.progressbar(iterable, label=description) as bar:
89
+ for item in bar:
90
+ yield item
91
+ except ImportError:
92
+ # Fallback if click not available
93
+ for item in iterable:
94
+ yield item