glaip-sdk 0.0.6a0__py3-none-any.whl → 0.0.7__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 CHANGED
@@ -24,28 +24,51 @@ except Exception: # pragma: no cover
24
24
  _toml = None # type: ignore
25
25
 
26
26
 
27
- def _get_version() -> str:
27
+ def _try_get_installed_version() -> str | None:
28
+ """Try to get version from installed package."""
28
29
  try:
29
30
  return version("glaip-sdk")
30
31
  except PackageNotFoundError:
31
- # Not installed; try reading from local pyproject for dev
32
- if _toml is not None:
33
- try:
34
- from pathlib import Path
35
-
36
- here = Path(__file__).resolve()
37
- root = here.parent.parent # project root (contains pyproject.toml)
38
- pyproject = root / "pyproject.toml"
39
- if pyproject.is_file():
40
- data = _toml.loads(pyproject.read_text(encoding="utf-8"))
41
- ver = data.get("project", {}).get("version") or data.get(
42
- "tool", {}
43
- ).get("poetry", {}).get("version")
44
- if isinstance(ver, str) and ver:
45
- return ver
46
- except Exception:
47
- pass
48
- return "0.0.0.dev0"
32
+ return None
33
+
34
+
35
+ def _try_get_dev_version() -> str | None:
36
+ """Try to get version from local pyproject.toml for development."""
37
+ if _toml is None:
38
+ return None
39
+
40
+ try:
41
+ from pathlib import Path
42
+
43
+ here = Path(__file__).resolve()
44
+ root = here.parent.parent # project root (contains pyproject.toml)
45
+ pyproject = root / "pyproject.toml"
46
+ if not pyproject.is_file():
47
+ return None
48
+
49
+ data = _toml.loads(pyproject.read_text(encoding="utf-8"))
50
+ ver = data.get("project", {}).get("version") or data.get("tool", {}).get(
51
+ "poetry", {}
52
+ ).get("version")
53
+ if isinstance(ver, str) and ver:
54
+ return ver
55
+ except Exception:
56
+ pass
57
+ return None
58
+
59
+
60
+ def _get_version() -> str:
61
+ # Try installed version first
62
+ installed_version = _try_get_installed_version()
63
+ if installed_version:
64
+ return installed_version
65
+
66
+ # Try development version
67
+ dev_version = _try_get_dev_version()
68
+ if dev_version:
69
+ return dev_version
70
+
71
+ return "0.0.0.dev0"
49
72
 
50
73
 
51
74
  __version__ = _get_version()
@@ -47,6 +47,7 @@ from glaip_sdk.cli.utils import (
47
47
  _fuzzy_pick_for_resources,
48
48
  build_renderer,
49
49
  coerce_to_row,
50
+ detect_export_format,
50
51
  get_client,
51
52
  get_ctx_value,
52
53
  output_flags,
@@ -180,7 +181,7 @@ def _format_fallback_agent_data(client: Any, agent: Any) -> dict:
180
181
  result_data = build_resource_result_data(full_agent, fields)
181
182
 
182
183
  # Handle missing instruction
183
- if not result_data.get("instruction"):
184
+ if result_data.get("instruction") in ["N/A", None, ""]:
184
185
  result_data["instruction"] = "-"
185
186
 
186
187
  # Format dates for better display
@@ -356,18 +357,15 @@ def get(ctx: Any, agent_ref: str, select: int | None, export: str | None) -> Non
356
357
  )
357
358
 
358
359
  # Handle export option
359
- if export: # pragma: no cover - requires filesystem verification
360
+ if export:
360
361
  export_path = Path(export)
361
362
  # Auto-detect format from file extension
362
- if export_path.suffix.lower() in [".yaml", ".yml"]:
363
- detected_format = "yaml"
364
- else:
365
- detected_format = "json"
363
+ detected_format = detect_export_format(export_path)
366
364
 
367
365
  # Always export comprehensive data - re-fetch agent with full details
368
366
  try:
369
367
  agent = client.agents.get_agent_by_id(agent.id)
370
- except Exception as e: # pragma: no cover - best-effort fallback messaging
368
+ except Exception as e:
371
369
  handle_rich_output(
372
370
  ctx,
373
371
  Text(
@@ -584,8 +582,7 @@ def run(
584
582
  handle_json_output(ctx, error=Exception(error_msg))
585
583
  raise click.ClickException(error_msg)
586
584
  except Exception as e:
587
- handle_json_output(ctx, error=e)
588
- raise click.ClickException(str(e))
585
+ _handle_command_exception(ctx, e)
589
586
 
590
587
 
591
588
  def _handle_import_file_logic(
@@ -798,8 +795,8 @@ def _handle_successful_creation(ctx: Any, agent: Any, model: str | None) -> None
798
795
  handle_rich_output(ctx, display_agent_run_suggestions(agent))
799
796
 
800
797
 
801
- def _handle_creation_exception(ctx: Any, e: Exception) -> None:
802
- """Handle exceptions during agent creation."""
798
+ def _handle_command_exception(ctx: Any, e: Exception) -> None:
799
+ """Handle exceptions during command execution with consistent error handling."""
803
800
  if isinstance(e, click.ClickException):
804
801
  if get_ctx_value(ctx, "view") == "json":
805
802
  handle_json_output(ctx, error=Exception(AGENT_NOT_FOUND_ERROR))
@@ -811,6 +808,11 @@ def _handle_creation_exception(ctx: Any, e: Exception) -> None:
811
808
  raise click.ClickException(str(e))
812
809
 
813
810
 
811
+ def _handle_creation_exception(ctx: Any, e: Exception) -> None:
812
+ """Handle exceptions during agent creation."""
813
+ _handle_command_exception(ctx, e)
814
+
815
+
814
816
  @agents_group.command()
815
817
  @click.option("--name", help="Agent name")
816
818
  @click.option("--instruction", help="Agent instruction (prompt)")
@@ -1089,10 +1091,7 @@ def update(
1089
1091
  # Re-raise ClickExceptions without additional processing
1090
1092
  raise
1091
1093
  except Exception as e:
1092
- handle_json_output(ctx, error=e)
1093
- if get_ctx_value(ctx, "view") != "json":
1094
- print_api_error(e)
1095
- raise click.ClickException(str(e))
1094
+ _handle_command_exception(ctx, e)
1096
1095
 
1097
1096
 
1098
1097
  @agents_group.command()
@@ -1133,10 +1132,7 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1133
1132
  # Re-raise ClickExceptions without additional processing
1134
1133
  raise
1135
1134
  except Exception as e:
1136
- handle_json_output(ctx, error=e)
1137
- if get_ctx_value(ctx, "view") != "json":
1138
- print_api_error(e)
1139
- raise click.ClickException(str(e))
1135
+ _handle_command_exception(ctx, e)
1140
1136
 
1141
1137
 
1142
1138
  @agents_group.command()
@@ -1149,9 +1145,7 @@ def delete(ctx: Any, agent_id: str, yes: bool) -> None:
1149
1145
  )
1150
1146
  @output_flags()
1151
1147
  @click.pass_context
1152
- def sync_langflow(
1153
- ctx: Any, base_url: str | None, api_key: str | None
1154
- ) -> None: # pragma: no cover - integration-only path
1148
+ def sync_langflow(ctx: Any, base_url: str | None, api_key: str | None) -> None:
1155
1149
  """Sync agents with LangFlow server flows.
1156
1150
 
1157
1151
  This command fetches all flows from the configured LangFlow server and
@@ -1192,7 +1186,4 @@ def sync_langflow(
1192
1186
  )
1193
1187
 
1194
1188
  except Exception as e:
1195
- handle_json_output(ctx, error=e)
1196
- if get_ctx_value(ctx, "view") != "json":
1197
- print_api_error(e)
1198
- raise click.ClickException(str(e))
1189
+ _handle_command_exception(ctx, e)
@@ -47,8 +47,8 @@ def save_config(config: dict[str, Any]) -> None:
47
47
  # Set secure file permissions
48
48
  try:
49
49
  os.chmod(CONFIG_FILE, 0o600)
50
- except Exception:
51
- pass # Best effort
50
+ except Exception: # pragma: no cover - platform dependent best effort
51
+ pass
52
52
 
53
53
 
54
54
  @click.group()
@@ -30,6 +30,7 @@ from glaip_sdk.cli.io import (
30
30
  from glaip_sdk.cli.resolution import resolve_resource_reference
31
31
  from glaip_sdk.cli.utils import (
32
32
  coerce_to_row,
33
+ detect_export_format,
33
34
  get_client,
34
35
  get_ctx_value,
35
36
  output_flags,
@@ -178,10 +179,7 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
178
179
  if export:
179
180
  export_path = Path(export)
180
181
  # Auto-detect format from file extension
181
- if export_path.suffix.lower() in [".yaml", ".yml"]:
182
- detected_format = "yaml"
183
- else:
184
- detected_format = "json"
182
+ detected_format = detect_export_format(export_path)
185
183
 
186
184
  # Always export comprehensive data - re-fetch MCP with full details if needed
187
185
  try:
@@ -34,6 +34,7 @@ from glaip_sdk.cli.io import (
34
34
  from glaip_sdk.cli.resolution import resolve_resource_reference
35
35
  from glaip_sdk.cli.utils import (
36
36
  coerce_to_row,
37
+ detect_export_format,
37
38
  get_client,
38
39
  get_ctx_value,
39
40
  output_flags,
@@ -328,10 +329,7 @@ def get(ctx: Any, tool_ref: str, select: int | None, export: str | None) -> None
328
329
  if export:
329
330
  export_path = Path(export)
330
331
  # Auto-detect format from file extension
331
- if export_path.suffix.lower() in [".yaml", ".yml"]:
332
- detected_format = "yaml"
333
- else:
334
- detected_format = "json"
332
+ detected_format = detect_export_format(export_path)
335
333
 
336
334
  # Always export comprehensive data - re-fetch tool with full details if needed
337
335
  try:
glaip_sdk/cli/display.py CHANGED
@@ -134,17 +134,18 @@ _MISSING = object()
134
134
  def build_resource_result_data(resource: Any, fields: list[str]) -> dict[str, Any]:
135
135
  """Return a normalized mapping of ``fields`` extracted from ``resource``."""
136
136
 
137
- return {
138
- field: _normalise_field_value(field, _safe_get_attr(resource, field))
139
- for field in fields
140
- }
137
+ result: dict[str, Any] = {}
138
+ for field in fields:
139
+ try:
140
+ value = getattr(resource, field)
141
+ except AttributeError:
142
+ value = _MISSING
143
+ except Exception:
144
+ value = _MISSING
141
145
 
146
+ result[field] = _normalise_field_value(field, value)
142
147
 
143
- def _safe_get_attr(resource: Any, field: str) -> Any:
144
- try:
145
- return getattr(resource, field, _MISSING)
146
- except Exception:
147
- return _MISSING
148
+ return result
148
149
 
149
150
 
150
151
  def _normalise_field_value(field: str, value: Any) -> Any:
@@ -188,9 +189,7 @@ def _build_success_output_data(data: Any) -> dict[str, Any]:
188
189
  return data if data is not None else {"success": True}
189
190
 
190
191
 
191
- def handle_json_output(
192
- ctx: Any, data: Any = None, error: Exception = None
193
- ) -> None: # pragma: no cover - formatting covered via integration tests
192
+ def handle_json_output(ctx: Any, data: Any = None, error: Exception = None) -> None:
194
193
  """Handle JSON output format for CLI commands.
195
194
 
196
195
  Args:
glaip_sdk/cli/utils.py CHANGED
@@ -15,6 +15,7 @@ import shutil
15
15
  import subprocess
16
16
  import tempfile
17
17
  from collections.abc import Callable
18
+ from pathlib import Path
18
19
  from typing import TYPE_CHECKING, Any
19
20
 
20
21
  import click
@@ -29,15 +30,15 @@ try:
29
30
  from prompt_toolkit.shortcuts import prompt
30
31
 
31
32
  _HAS_PTK = True
32
- except Exception:
33
+ except Exception: # pragma: no cover - optional dependency
33
34
  _HAS_PTK = False
34
35
 
35
36
  try:
36
37
  import questionary
37
- except Exception:
38
+ except Exception: # pragma: no cover - optional dependency
38
39
  questionary = None
39
40
 
40
- if TYPE_CHECKING:
41
+ if TYPE_CHECKING: # pragma: no cover - import-only during type checking
41
42
  from glaip_sdk import Client
42
43
  from glaip_sdk.cli.commands.configure import load_config
43
44
  from glaip_sdk.rich_components import AIPPanel, AIPTable
@@ -100,7 +101,7 @@ def _prepare_pager_env(
100
101
 
101
102
  def _render_ansi(
102
103
  renderable: Any,
103
- ) -> str: # pragma: no cover - rendering requires real terminal
104
+ ) -> str:
104
105
  """Render a Rich renderable to an ANSI string suitable for piping to 'less'."""
105
106
  buf = io.StringIO()
106
107
  tmp_console = Console(
@@ -116,7 +117,7 @@ def _render_ansi(
116
117
  return buf.getvalue()
117
118
 
118
119
 
119
- def _pager_header() -> str: # pragma: no cover - terminal UI helper
120
+ def _pager_header() -> str:
120
121
  v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
121
122
  if v in {"0", "false", "off"}:
122
123
  return ""
@@ -227,7 +228,7 @@ def _get_view(ctx: Any) -> str:
227
228
  # ----------------------------- Client config ----------------------------- #
228
229
 
229
230
 
230
- def get_client(ctx: Any) -> Client:
231
+ def get_client(ctx: Any) -> Client: # pragma: no cover
231
232
  """Get configured client from context, env, and config file (ctx > env > file)."""
232
233
  from glaip_sdk import Client
233
234
 
@@ -433,7 +434,9 @@ class _FuzzyCompleter:
433
434
  def __init__(self, words: list[str]) -> None:
434
435
  self.words = words
435
436
 
436
- def get_completions(self, document: Any, _complete_event: Any) -> Any:
437
+ def get_completions(
438
+ self, document: Any, _complete_event: Any
439
+ ) -> Any: # pragma: no cover
437
440
  word = document.get_word_before_cursor()
438
441
  if not word:
439
442
  return
@@ -444,7 +447,7 @@ class _FuzzyCompleter:
444
447
  if self._fuzzy_match(word_lower, label_lower):
445
448
  yield Completion(label, start_position=-len(word))
446
449
 
447
- def _fuzzy_match(self, search: str, target: str) -> bool:
450
+ def _fuzzy_match(self, search: str, target: str) -> bool: # pragma: no cover
448
451
  """True fuzzy matching: checks if all characters in search appear in order in target."""
449
452
  if not search:
450
453
  return True
@@ -502,47 +505,37 @@ def _fuzzy_pick(
502
505
  complete_in_thread=True,
503
506
  complete_while_typing=True,
504
507
  )
505
- except (KeyboardInterrupt, EOFError):
508
+ except (KeyboardInterrupt, EOFError): # pragma: no cover - user cancelled input
506
509
  return None
507
510
 
508
511
  return _perform_fuzzy_search(answer, labels, by_label) if answer else None
509
512
 
510
513
 
511
- def _fuzzy_score(search: str, target: str) -> int:
512
- """
513
- Calculate fuzzy match score.
514
- Higher score = better match.
515
- Returns -1 if no match possible.
516
- """
514
+ def _is_fuzzy_match(search: str, target: str) -> bool:
515
+ """Check if search string is a fuzzy match for target."""
517
516
  if not search:
518
- return 0
517
+ return True
519
518
 
520
- # Check if it's a fuzzy match first
521
519
  search_idx = 0
522
520
  for char in target:
523
521
  if search_idx < len(search) and search[search_idx] == char:
524
522
  search_idx += 1
525
523
  if search_idx == len(search):
526
- break
527
-
528
- if search_idx < len(search):
529
- return -1 # Not a fuzzy match
524
+ return True
525
+ return False
530
526
 
531
- # Calculate score based on:
532
- # 1. Exact substring match gets bonus points
533
- # 2. Consecutive character matches get bonus points
534
- # 3. Shorter search terms get bonus points
535
527
 
536
- score = 0
528
+ def _calculate_exact_match_bonus(search: str, target: str) -> int:
529
+ """Calculate bonus for exact substring matches."""
530
+ return 100 if search.lower() in target.lower() else 0
537
531
 
538
- # Exact substring bonus
539
- if search.lower() in target.lower():
540
- score += 100
541
532
 
542
- # Consecutive character bonus
533
+ def _calculate_consecutive_bonus(search: str, target: str) -> int:
534
+ """Calculate bonus for consecutive character matches."""
543
535
  consecutive = 0
544
536
  max_consecutive = 0
545
537
  search_idx = 0
538
+
546
539
  for char in target:
547
540
  if search_idx < len(search) and search[search_idx] == char:
548
541
  consecutive += 1
@@ -551,10 +544,31 @@ def _fuzzy_score(search: str, target: str) -> int:
551
544
  else:
552
545
  consecutive = 0
553
546
 
554
- score += max_consecutive * 10
547
+ return max_consecutive * 10
548
+
555
549
 
556
- # Length bonus (shorter searches get higher scores)
557
- score += (len(target) - len(search)) * 2
550
+ def _calculate_length_bonus(search: str, target: str) -> int:
551
+ """Calculate bonus for shorter search terms."""
552
+ return (len(target) - len(search)) * 2
553
+
554
+
555
+ def _fuzzy_score(search: str, target: str) -> int:
556
+ """
557
+ Calculate fuzzy match score.
558
+ Higher score = better match.
559
+ Returns -1 if no match possible.
560
+ """
561
+ if not search:
562
+ return 0
563
+
564
+ if not _is_fuzzy_match(search, target):
565
+ return -1 # Not a fuzzy match
566
+
567
+ # Calculate score based on different factors
568
+ score = 0
569
+ score += _calculate_exact_match_bonus(search, target)
570
+ score += _calculate_consecutive_bonus(search, target)
571
+ score += _calculate_length_bonus(search, target)
558
572
 
559
573
  return score
560
574
 
@@ -1222,3 +1236,19 @@ def handle_ambiguous_resource(
1222
1236
  else:
1223
1237
  # Re-raise cancellation exceptions
1224
1238
  raise
1239
+
1240
+
1241
+ def detect_export_format(file_path: str | Path) -> str:
1242
+ """Detect export format from file extension.
1243
+
1244
+ Args:
1245
+ file_path: Path to the export file
1246
+
1247
+ Returns:
1248
+ "yaml" if file extension is .yaml or .yml, "json" otherwise
1249
+ """
1250
+ path = Path(file_path)
1251
+ if path.suffix.lower() in [".yaml", ".yml"]:
1252
+ return "yaml"
1253
+ else:
1254
+ return "json"