glaip-sdk 0.0.14__py3-none-any.whl → 0.0.15__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/commands/agents.py +1 -3
- glaip_sdk/cli/commands/mcps.py +1 -3
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +1 -3
- glaip_sdk/cli/context.py +142 -0
- glaip_sdk/cli/masking.py +148 -0
- glaip_sdk/cli/pager.py +271 -0
- glaip_sdk/cli/utils.py +9 -336
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.15.dist-info}/METADATA +1 -1
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.15.dist-info}/RECORD +12 -9
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.15.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.14.dist-info → glaip_sdk-0.0.15.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -23,6 +23,7 @@ from glaip_sdk.cli.agent_config import (
|
|
|
23
23
|
from glaip_sdk.cli.agent_config import (
|
|
24
24
|
sanitize_agent_config_for_cli as sanitize_agent_config,
|
|
25
25
|
)
|
|
26
|
+
from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
|
|
26
27
|
from glaip_sdk.cli.display import (
|
|
27
28
|
build_resource_result_data,
|
|
28
29
|
display_agent_run_suggestions,
|
|
@@ -48,10 +49,7 @@ from glaip_sdk.cli.utils import (
|
|
|
48
49
|
_fuzzy_pick_for_resources,
|
|
49
50
|
build_renderer,
|
|
50
51
|
coerce_to_row,
|
|
51
|
-
detect_export_format,
|
|
52
52
|
get_client,
|
|
53
|
-
get_ctx_value,
|
|
54
|
-
output_flags,
|
|
55
53
|
output_list,
|
|
56
54
|
output_result,
|
|
57
55
|
spinner_context,
|
glaip_sdk/cli/commands/mcps.py
CHANGED
|
@@ -13,6 +13,7 @@ import click
|
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
from rich.text import Text
|
|
15
15
|
|
|
16
|
+
from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
|
|
16
17
|
from glaip_sdk.cli.display import (
|
|
17
18
|
display_api_error,
|
|
18
19
|
display_confirmation_prompt,
|
|
@@ -34,10 +35,7 @@ from glaip_sdk.cli.parsers.json_input import parse_json_input
|
|
|
34
35
|
from glaip_sdk.cli.resolution import resolve_resource_reference
|
|
35
36
|
from glaip_sdk.cli.utils import (
|
|
36
37
|
coerce_to_row,
|
|
37
|
-
detect_export_format,
|
|
38
38
|
get_client,
|
|
39
|
-
get_ctx_value,
|
|
40
|
-
output_flags,
|
|
41
39
|
output_list,
|
|
42
40
|
output_result,
|
|
43
41
|
spinner_context,
|
glaip_sdk/cli/commands/models.py
CHANGED
glaip_sdk/cli/commands/tools.py
CHANGED
|
@@ -13,6 +13,7 @@ import click
|
|
|
13
13
|
from rich.console import Console
|
|
14
14
|
from rich.text import Text
|
|
15
15
|
|
|
16
|
+
from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
|
|
16
17
|
from glaip_sdk.cli.display import (
|
|
17
18
|
display_api_error,
|
|
18
19
|
display_confirmation_prompt,
|
|
@@ -34,10 +35,7 @@ from glaip_sdk.cli.io import (
|
|
|
34
35
|
from glaip_sdk.cli.resolution import resolve_resource_reference
|
|
35
36
|
from glaip_sdk.cli.utils import (
|
|
36
37
|
coerce_to_row,
|
|
37
|
-
detect_export_format,
|
|
38
38
|
get_client,
|
|
39
|
-
get_ctx_value,
|
|
40
|
-
output_flags,
|
|
41
39
|
output_list,
|
|
42
40
|
output_result,
|
|
43
41
|
spinner_context,
|
glaip_sdk/cli/context.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Context-related helpers for the glaip CLI.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"get_ctx_value",
|
|
17
|
+
"_get_view",
|
|
18
|
+
"_set_view",
|
|
19
|
+
"_set_json",
|
|
20
|
+
"output_flags",
|
|
21
|
+
"detect_export_format",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_ctx_value(ctx: Any, key: str, default: Any = None) -> Any:
|
|
26
|
+
"""Safely resolve a value from a click context object.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
ctx: Click context object to extract value from
|
|
30
|
+
key: Key to retrieve from the context
|
|
31
|
+
default: Default value if key is not found
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The value associated with the key, or the default if not found
|
|
35
|
+
"""
|
|
36
|
+
if ctx is None:
|
|
37
|
+
return default
|
|
38
|
+
|
|
39
|
+
obj = getattr(ctx, "obj", None)
|
|
40
|
+
if obj is None:
|
|
41
|
+
return default
|
|
42
|
+
|
|
43
|
+
if isinstance(obj, dict):
|
|
44
|
+
return obj.get(key, default)
|
|
45
|
+
|
|
46
|
+
getter = getattr(obj, "get", None)
|
|
47
|
+
if callable(getter):
|
|
48
|
+
try:
|
|
49
|
+
return getter(key, default)
|
|
50
|
+
except TypeError:
|
|
51
|
+
return default
|
|
52
|
+
|
|
53
|
+
return getattr(obj, key, default) if hasattr(obj, key) else default
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _get_view(ctx: Any) -> str:
|
|
57
|
+
"""Resolve the active view preference from context.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
ctx: Click context object containing view preferences
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
The view format string (rich, plain, json, md), defaults to 'rich'
|
|
64
|
+
"""
|
|
65
|
+
view = get_ctx_value(ctx, "view")
|
|
66
|
+
if view:
|
|
67
|
+
return view
|
|
68
|
+
|
|
69
|
+
fallback = get_ctx_value(ctx, "format")
|
|
70
|
+
return fallback or "rich"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _set_view(ctx: Any, _param: Any, value: str) -> None:
|
|
74
|
+
"""Click callback to persist the `--view/--output` option.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
ctx: Click context object to store the view preference
|
|
78
|
+
_param: Click parameter object (unused)
|
|
79
|
+
value: The view format string to store
|
|
80
|
+
"""
|
|
81
|
+
if not value:
|
|
82
|
+
return
|
|
83
|
+
ctx.ensure_object(dict)
|
|
84
|
+
ctx.obj["view"] = value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _set_json(ctx: Any, _param: Any, value: bool) -> None:
|
|
88
|
+
"""Click callback for the `--json` shorthand flag.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
ctx: Click context object to store the view preference
|
|
92
|
+
_param: Click parameter object (unused)
|
|
93
|
+
value: Boolean flag indicating json mode
|
|
94
|
+
"""
|
|
95
|
+
if not value:
|
|
96
|
+
return
|
|
97
|
+
ctx.ensure_object(dict)
|
|
98
|
+
ctx.obj["view"] = "json"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def output_flags() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
102
|
+
"""Decorator to add shared output flags (`--view`, `--json`) to commands.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A decorator function that adds output format options to click commands
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
109
|
+
f = click.option(
|
|
110
|
+
"--json",
|
|
111
|
+
"json_mode",
|
|
112
|
+
is_flag=True,
|
|
113
|
+
expose_value=False,
|
|
114
|
+
help="Shortcut for --view json",
|
|
115
|
+
callback=_set_json,
|
|
116
|
+
)(f)
|
|
117
|
+
f = click.option(
|
|
118
|
+
"-o",
|
|
119
|
+
"--output",
|
|
120
|
+
"--view",
|
|
121
|
+
"view_opt",
|
|
122
|
+
type=click.Choice(["rich", "plain", "json", "md"]),
|
|
123
|
+
expose_value=False,
|
|
124
|
+
help="Output format",
|
|
125
|
+
callback=_set_view,
|
|
126
|
+
)(f)
|
|
127
|
+
return f
|
|
128
|
+
|
|
129
|
+
return decorator
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def detect_export_format(file_path: str | Path) -> str:
|
|
133
|
+
"""Detect the export format from the file extension.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
file_path: Path to the file to analyze
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
The format string ('yaml' or 'json') based on file extension
|
|
140
|
+
"""
|
|
141
|
+
path = Path(file_path)
|
|
142
|
+
return "yaml" if path.suffix.lower() in {".yaml", ".yml"} else "json"
|
glaip_sdk/cli/masking.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Masking helpers for CLI output.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"mask_payload",
|
|
14
|
+
"mask_rows",
|
|
15
|
+
"_mask_value",
|
|
16
|
+
"_mask_any",
|
|
17
|
+
"_maybe_mask_row",
|
|
18
|
+
"_resolve_mask_fields",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
_DEFAULT_MASK_FIELDS = {
|
|
22
|
+
"api_key",
|
|
23
|
+
"apikey",
|
|
24
|
+
"token",
|
|
25
|
+
"access_token",
|
|
26
|
+
"secret",
|
|
27
|
+
"client_secret",
|
|
28
|
+
"password",
|
|
29
|
+
"private_key",
|
|
30
|
+
"bearer",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _mask_value(raw: Any) -> str:
|
|
35
|
+
"""Return a masked representation of the provided value.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
raw: The raw value to mask, converted to string.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: A masked representation showing first 4 and last 4 characters
|
|
42
|
+
separated by dots, or "••••" for strings ≤ 8 characters.
|
|
43
|
+
"""
|
|
44
|
+
text = str(raw)
|
|
45
|
+
if len(text) <= 8:
|
|
46
|
+
return "••••"
|
|
47
|
+
return f"{text[:4]}••••••••{text[-4:]}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _mask_any(value: Any, mask_fields: set[str]) -> Any:
|
|
51
|
+
"""Recursively mask sensitive fields in mappings and iterables.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
value: The value to process - can be dict, list, or any other type.
|
|
55
|
+
mask_fields: Set of field names (lowercase) that should be masked.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Any: The processed value with sensitive fields masked. Dicts and lists
|
|
59
|
+
are processed recursively, other values are returned unchanged.
|
|
60
|
+
"""
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
masked: dict[Any, Any] = {}
|
|
63
|
+
for key, raw in value.items():
|
|
64
|
+
if isinstance(key, str) and key.lower() in mask_fields and raw is not None:
|
|
65
|
+
masked[key] = _mask_value(raw)
|
|
66
|
+
else:
|
|
67
|
+
masked[key] = _mask_any(raw, mask_fields)
|
|
68
|
+
return masked
|
|
69
|
+
|
|
70
|
+
if isinstance(value, list):
|
|
71
|
+
return [_mask_any(item, mask_fields) for item in value]
|
|
72
|
+
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any]:
|
|
77
|
+
"""Mask a single row when masking is enabled.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
row: A dictionary representing a single row of data.
|
|
81
|
+
mask_fields: Set of field names to mask. If empty, returns row unchanged.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
dict[str, Any]: The row with sensitive fields masked, or the original
|
|
85
|
+
row if no mask_fields are provided.
|
|
86
|
+
"""
|
|
87
|
+
if not mask_fields:
|
|
88
|
+
return row
|
|
89
|
+
return _mask_any(row, mask_fields)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _resolve_mask_fields() -> set[str]:
|
|
93
|
+
"""Resolve the set of sensitive fields to mask based on environment.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
set[str]: Set of field names to mask. Empty set if masking is disabled
|
|
97
|
+
via AIP_MASK_OFF environment variable, custom fields from
|
|
98
|
+
AIP_MASK_FIELDS, or default fields if neither is set.
|
|
99
|
+
"""
|
|
100
|
+
if os.getenv("AIP_MASK_OFF", "0") in {"1", "true", "on", "yes"}:
|
|
101
|
+
return set()
|
|
102
|
+
|
|
103
|
+
env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
|
|
104
|
+
if env_fields:
|
|
105
|
+
parts = [part.strip().lower() for part in env_fields.split(",") if part.strip()]
|
|
106
|
+
return set(parts)
|
|
107
|
+
|
|
108
|
+
return set(_DEFAULT_MASK_FIELDS)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def mask_payload(payload: Any) -> Any:
|
|
112
|
+
"""Mask sensitive values in an arbitrary payload when masking is enabled.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
payload: Any data structure (dict, list, or primitive) to mask.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Any: The payload with sensitive fields masked based on environment
|
|
119
|
+
configuration. Returns original payload if masking is disabled
|
|
120
|
+
or if an error occurs during masking.
|
|
121
|
+
"""
|
|
122
|
+
mask_fields = _resolve_mask_fields()
|
|
123
|
+
if not mask_fields:
|
|
124
|
+
return payload
|
|
125
|
+
try:
|
|
126
|
+
return _mask_any(payload, mask_fields)
|
|
127
|
+
except Exception:
|
|
128
|
+
return payload
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def mask_rows(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
132
|
+
"""Mask sensitive values in row-oriented data when masking is enabled.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
rows: List of dictionaries representing rows of tabular data.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
list[dict[str, Any]]: List of rows with sensitive fields masked based
|
|
139
|
+
on environment configuration. Returns original
|
|
140
|
+
rows if masking is disabled or if an error occurs.
|
|
141
|
+
"""
|
|
142
|
+
mask_fields = _resolve_mask_fields()
|
|
143
|
+
if not mask_fields:
|
|
144
|
+
return rows
|
|
145
|
+
try:
|
|
146
|
+
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
147
|
+
except Exception:
|
|
148
|
+
return rows
|
glaip_sdk/cli/pager.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Pager-related helpers for CLI output.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import io
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shlex
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"console",
|
|
23
|
+
"_prepare_pager_env",
|
|
24
|
+
"_render_ansi",
|
|
25
|
+
"_pager_header",
|
|
26
|
+
"_should_use_pager",
|
|
27
|
+
"_resolve_pager_command",
|
|
28
|
+
"_run_less_pager",
|
|
29
|
+
"_run_more_pager",
|
|
30
|
+
"_run_pager_with_temp_file",
|
|
31
|
+
"_page_with_system_pager",
|
|
32
|
+
"_should_page_output",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
console: Console | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_console() -> Console:
|
|
39
|
+
"""Return the active console instance.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Console: The active Rich console instance
|
|
43
|
+
"""
|
|
44
|
+
global console
|
|
45
|
+
try:
|
|
46
|
+
from glaip_sdk.cli import utils as cli_utils
|
|
47
|
+
except Exception: # pragma: no cover - fallback during import cycles
|
|
48
|
+
cli_utils = None
|
|
49
|
+
|
|
50
|
+
current_console = getattr(cli_utils, "console", None) if cli_utils else None
|
|
51
|
+
if current_console is not None and current_console is not console:
|
|
52
|
+
console = current_console
|
|
53
|
+
|
|
54
|
+
if console is None:
|
|
55
|
+
console = Console()
|
|
56
|
+
return console
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _prepare_pager_env(clear_on_exit: bool = True) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Configure LESS flags for a predictable, high-quality UX:
|
|
62
|
+
-R : pass ANSI color escapes
|
|
63
|
+
-S : chop long lines (horizontal scroll with ←/→)
|
|
64
|
+
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
65
|
+
Toggle wrapping with AIP_PAGER_WRAP=1 to drop -S.
|
|
66
|
+
Power users can override via AIP_LESS_FLAGS.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
clear_on_exit: Whether to clear the pager on exit (default: True)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
None
|
|
73
|
+
"""
|
|
74
|
+
os.environ.pop("LESSSECURE", None)
|
|
75
|
+
if os.getenv("LESS") is None:
|
|
76
|
+
want_wrap = os.getenv("AIP_PAGER_WRAP", "0") == "1"
|
|
77
|
+
base = "-R" if want_wrap else "-RS"
|
|
78
|
+
default_flags = base if clear_on_exit else (base + "FX")
|
|
79
|
+
os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _render_ansi(renderable: Any) -> str:
|
|
83
|
+
"""Render a Rich renderable to an ANSI string suitable for piping to 'less'.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
renderable: Any Rich-compatible renderable object
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str: ANSI string representation of the renderable
|
|
90
|
+
"""
|
|
91
|
+
active_console = _get_console()
|
|
92
|
+
buf = io.StringIO()
|
|
93
|
+
tmp_console = Console(
|
|
94
|
+
file=buf,
|
|
95
|
+
force_terminal=True,
|
|
96
|
+
color_system=active_console.color_system or "auto",
|
|
97
|
+
width=active_console.size.width or 100,
|
|
98
|
+
legacy_windows=False,
|
|
99
|
+
soft_wrap=False,
|
|
100
|
+
record=False,
|
|
101
|
+
)
|
|
102
|
+
tmp_console.print(renderable)
|
|
103
|
+
return buf.getvalue()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _pager_header() -> str:
|
|
107
|
+
"""Generate pager header with navigation instructions.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
str: Header text containing navigation help, or empty string if disabled
|
|
111
|
+
"""
|
|
112
|
+
v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
|
|
113
|
+
if v in {"0", "false", "off"}:
|
|
114
|
+
return ""
|
|
115
|
+
return "\n".join(
|
|
116
|
+
[
|
|
117
|
+
"TABLE VIEW — ↑/↓ PgUp/PgDn, ←/→ horiz scroll (with -S), /search, n/N next/prev, h help, q quit",
|
|
118
|
+
"───────────────────────────────────────────────────────────────────────────────────────────────",
|
|
119
|
+
"",
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _should_use_pager() -> bool:
|
|
125
|
+
"""Check if we should attempt to use a system pager.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
bool: True if we should use a pager, False otherwise
|
|
129
|
+
"""
|
|
130
|
+
active_console = _get_console()
|
|
131
|
+
if not (active_console.is_terminal and os.isatty(1)):
|
|
132
|
+
return False
|
|
133
|
+
if (os.getenv("TERM") or "").lower() == "dumb":
|
|
134
|
+
return False
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _resolve_pager_command() -> tuple[list[str] | None, str | None]:
|
|
139
|
+
"""Resolve the pager command and path to use.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
tuple[list[str] | None, str | None]: A tuple containing:
|
|
143
|
+
- list[str] | None: The pager command parts if PAGER is set to less, None otherwise
|
|
144
|
+
- str | None: The path to the less executable if found, None otherwise
|
|
145
|
+
"""
|
|
146
|
+
pager_cmd = None
|
|
147
|
+
pager_env = os.getenv("PAGER")
|
|
148
|
+
if pager_env:
|
|
149
|
+
parts = shlex.split(pager_env)
|
|
150
|
+
if parts and os.path.basename(parts[0]).lower() == "less":
|
|
151
|
+
pager_cmd = parts
|
|
152
|
+
|
|
153
|
+
less_path = shutil.which("less")
|
|
154
|
+
return pager_cmd, less_path
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _run_less_pager(
|
|
158
|
+
pager_cmd: list[str] | None, less_path: str | None, tmp_path: str
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Run less pager with appropriate command and flags.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
pager_cmd: Custom pager command parts if PAGER is set to less, None otherwise
|
|
164
|
+
less_path: Path to the less executable, None if not found
|
|
165
|
+
tmp_path: Path to temporary file containing content to display
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
None
|
|
169
|
+
"""
|
|
170
|
+
if pager_cmd:
|
|
171
|
+
subprocess.run([*pager_cmd, tmp_path], check=False)
|
|
172
|
+
else:
|
|
173
|
+
flags = os.getenv("LESS", "-RS").split()
|
|
174
|
+
subprocess.run([less_path, *flags, tmp_path], check=False)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _run_more_pager(tmp_path: str) -> None:
|
|
178
|
+
"""Run more pager as fallback.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
tmp_path: Path to temporary file containing content to display
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
None
|
|
185
|
+
|
|
186
|
+
Raises:
|
|
187
|
+
FileNotFoundError: If more command is not found
|
|
188
|
+
"""
|
|
189
|
+
more_path = shutil.which("more")
|
|
190
|
+
if more_path:
|
|
191
|
+
subprocess.run([more_path, tmp_path], check=False)
|
|
192
|
+
else:
|
|
193
|
+
raise FileNotFoundError("more command not found")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _run_pager_with_temp_file(
|
|
197
|
+
pager_runner: Callable[[str], None], ansi_text: str
|
|
198
|
+
) -> bool:
|
|
199
|
+
"""Run a pager using a temporary file containing the content.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
pager_runner: Function that takes a temp file path and runs the pager
|
|
203
|
+
ansi_text: ANSI-formatted text content to display
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
bool: True if pager executed successfully, False if there was an exception
|
|
207
|
+
"""
|
|
208
|
+
_prepare_pager_env(clear_on_exit=True)
|
|
209
|
+
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tmp:
|
|
210
|
+
tmp.write(_pager_header())
|
|
211
|
+
tmp.write(ansi_text)
|
|
212
|
+
tmp_path = tmp.name
|
|
213
|
+
try:
|
|
214
|
+
pager_runner(tmp_path)
|
|
215
|
+
return True
|
|
216
|
+
except Exception:
|
|
217
|
+
return False
|
|
218
|
+
finally:
|
|
219
|
+
try:
|
|
220
|
+
os.unlink(tmp_path)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _page_with_system_pager(ansi_text: str) -> bool:
|
|
226
|
+
"""Prefer 'less' with a temp file so stdin remains the TTY.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
ansi_text: ANSI-formatted text content to display in the pager
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
bool: True if pager was executed successfully, False otherwise
|
|
233
|
+
"""
|
|
234
|
+
if not _should_use_pager():
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
pager_cmd, less_path = _resolve_pager_command()
|
|
238
|
+
|
|
239
|
+
if pager_cmd or less_path:
|
|
240
|
+
return _run_pager_with_temp_file(
|
|
241
|
+
lambda tmp_path: _run_less_pager(pager_cmd, less_path, tmp_path), ansi_text
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if platform.system().lower().startswith("win"):
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
return _run_pager_with_temp_file(_run_more_pager, ansi_text)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
251
|
+
"""Determine if output should be paginated based on content size and terminal.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
row_count: Number of rows in the content to display
|
|
255
|
+
is_tty: Whether the output is going to a terminal
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
bool: True if output should be paginated, False otherwise
|
|
259
|
+
"""
|
|
260
|
+
active_console = _get_console()
|
|
261
|
+
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
262
|
+
if pager_env in ("0", "off", "false"):
|
|
263
|
+
return False
|
|
264
|
+
if pager_env in ("1", "on", "true"):
|
|
265
|
+
return is_tty
|
|
266
|
+
try:
|
|
267
|
+
term_h = active_console.size.height or 24
|
|
268
|
+
approx_lines = 5 + row_count
|
|
269
|
+
return is_tty and (approx_lines >= term_h * 0.5)
|
|
270
|
+
except Exception:
|
|
271
|
+
return is_tty
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -2,23 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Authors:
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
|
-
import io
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
|
-
import platform
|
|
14
|
-
import shlex
|
|
15
|
-
import shutil
|
|
16
|
-
import subprocess
|
|
17
13
|
import sys
|
|
18
|
-
import tempfile
|
|
19
14
|
from collections.abc import Callable
|
|
20
15
|
from contextlib import AbstractContextManager, nullcontext
|
|
21
|
-
from pathlib import Path
|
|
22
16
|
from typing import TYPE_CHECKING, Any
|
|
23
17
|
|
|
24
18
|
import click
|
|
@@ -43,7 +37,9 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
43
37
|
|
|
44
38
|
if TYPE_CHECKING: # pragma: no cover - import-only during type checking
|
|
45
39
|
from glaip_sdk import Client
|
|
40
|
+
from glaip_sdk.cli import masking, pager
|
|
46
41
|
from glaip_sdk.cli.commands.configure import load_config
|
|
42
|
+
from glaip_sdk.cli.context import _get_view, get_ctx_value
|
|
47
43
|
from glaip_sdk.rich_components import AIPPanel, AIPTable
|
|
48
44
|
from glaip_sdk.utils import is_uuid
|
|
49
45
|
from glaip_sdk.utils.rendering.renderer import (
|
|
@@ -53,182 +49,10 @@ from glaip_sdk.utils.rendering.renderer import (
|
|
|
53
49
|
)
|
|
54
50
|
|
|
55
51
|
console = Console()
|
|
52
|
+
pager.console = console
|
|
56
53
|
logger = logging.getLogger("glaip_sdk.cli.utils")
|
|
57
54
|
|
|
58
55
|
|
|
59
|
-
# ----------------------------- Context helpers ---------------------------- #
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def get_ctx_value(ctx: Any, key: str, default: Any = None) -> Any:
|
|
63
|
-
"""Safely resolve a value from click's context object."""
|
|
64
|
-
if ctx is None:
|
|
65
|
-
return default
|
|
66
|
-
|
|
67
|
-
obj = getattr(ctx, "obj", None)
|
|
68
|
-
if obj is None:
|
|
69
|
-
return default
|
|
70
|
-
|
|
71
|
-
if isinstance(obj, dict):
|
|
72
|
-
return obj.get(key, default)
|
|
73
|
-
|
|
74
|
-
getter = getattr(obj, "get", None)
|
|
75
|
-
if callable(getter):
|
|
76
|
-
try:
|
|
77
|
-
return getter(key, default)
|
|
78
|
-
except TypeError:
|
|
79
|
-
return default
|
|
80
|
-
|
|
81
|
-
return getattr(obj, key, default) if hasattr(obj, key) else default
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
# ----------------------------- Pager helpers ----------------------------- #
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _prepare_pager_env(
|
|
88
|
-
clear_on_exit: bool = True,
|
|
89
|
-
) -> None: # pragma: no cover - terminal UI setup
|
|
90
|
-
"""
|
|
91
|
-
Configure LESS flags for a predictable, high-quality UX:
|
|
92
|
-
-R : pass ANSI color escapes
|
|
93
|
-
-S : chop long lines (horizontal scroll with ←/→)
|
|
94
|
-
(No -F, no -X) so we open a full-screen pager and clear on exit.
|
|
95
|
-
Toggle wrapping with AIP_PAGER_WRAP=1 to drop -S.
|
|
96
|
-
Power users can override via AIP_LESS_FLAGS.
|
|
97
|
-
"""
|
|
98
|
-
os.environ.pop("LESSSECURE", None)
|
|
99
|
-
if os.getenv("LESS") is None:
|
|
100
|
-
want_wrap = os.getenv("AIP_PAGER_WRAP", "0") == "1"
|
|
101
|
-
base = "-R" if want_wrap else "-RS"
|
|
102
|
-
default_flags = base if clear_on_exit else (base + "FX")
|
|
103
|
-
os.environ["LESS"] = os.getenv("AIP_LESS_FLAGS", default_flags)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def _render_ansi(
|
|
107
|
-
renderable: Any,
|
|
108
|
-
) -> str:
|
|
109
|
-
"""Render a Rich renderable to an ANSI string suitable for piping to 'less'."""
|
|
110
|
-
buf = io.StringIO()
|
|
111
|
-
tmp_console = Console(
|
|
112
|
-
file=buf,
|
|
113
|
-
force_terminal=True,
|
|
114
|
-
color_system=console.color_system or "auto",
|
|
115
|
-
width=console.size.width or 100,
|
|
116
|
-
legacy_windows=False,
|
|
117
|
-
soft_wrap=False,
|
|
118
|
-
record=False,
|
|
119
|
-
)
|
|
120
|
-
tmp_console.print(renderable)
|
|
121
|
-
return buf.getvalue()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _pager_header() -> str:
|
|
125
|
-
v = (os.getenv("AIP_PAGER_HEADER", "1") or "1").strip().lower()
|
|
126
|
-
if v in {"0", "false", "off"}:
|
|
127
|
-
return ""
|
|
128
|
-
return "\n".join(
|
|
129
|
-
[
|
|
130
|
-
"TABLE VIEW — ↑/↓ PgUp/PgDn, ←/→ horiz scroll (with -S), /search, n/N next/prev, h help, q quit",
|
|
131
|
-
"───────────────────────────────────────────────────────────────────────────────────────────────",
|
|
132
|
-
"",
|
|
133
|
-
]
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def _should_use_pager() -> bool:
|
|
138
|
-
"""Check if we should attempt to use a system pager."""
|
|
139
|
-
if not (console.is_terminal and os.isatty(1)):
|
|
140
|
-
return False
|
|
141
|
-
if (os.getenv("TERM") or "").lower() == "dumb":
|
|
142
|
-
return False
|
|
143
|
-
return True
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def _resolve_pager_command() -> tuple[list[str] | None, str | None]:
|
|
147
|
-
"""Resolve the pager command and path to use."""
|
|
148
|
-
pager_cmd = None
|
|
149
|
-
pager_env = os.getenv("PAGER")
|
|
150
|
-
if pager_env:
|
|
151
|
-
parts = shlex.split(pager_env)
|
|
152
|
-
if parts and os.path.basename(parts[0]).lower() == "less":
|
|
153
|
-
pager_cmd = parts
|
|
154
|
-
|
|
155
|
-
less_path = shutil.which("less")
|
|
156
|
-
return pager_cmd, less_path
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def _run_less_pager(
|
|
160
|
-
pager_cmd: list[str] | None, less_path: str | None, tmp_path: str
|
|
161
|
-
) -> None:
|
|
162
|
-
"""Run less pager with appropriate command and flags."""
|
|
163
|
-
if pager_cmd:
|
|
164
|
-
subprocess.run([*pager_cmd, tmp_path], check=False)
|
|
165
|
-
else:
|
|
166
|
-
flags = os.getenv("LESS", "-RS").split()
|
|
167
|
-
subprocess.run([less_path, *flags, tmp_path], check=False)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def _run_more_pager(tmp_path: str) -> None:
|
|
171
|
-
"""Run more pager as fallback."""
|
|
172
|
-
more_path = shutil.which("more")
|
|
173
|
-
if more_path:
|
|
174
|
-
subprocess.run([more_path, tmp_path], check=False)
|
|
175
|
-
else:
|
|
176
|
-
raise FileNotFoundError("more command not found")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def _run_pager_with_temp_file(
|
|
180
|
-
pager_runner: Callable[[str], None], ansi_text: str
|
|
181
|
-
) -> bool:
|
|
182
|
-
"""Run a pager using a temporary file containing the content."""
|
|
183
|
-
_prepare_pager_env(clear_on_exit=True)
|
|
184
|
-
with tempfile.NamedTemporaryFile("w", delete=False, encoding="utf-8") as tmp:
|
|
185
|
-
tmp.write(_pager_header())
|
|
186
|
-
tmp.write(ansi_text)
|
|
187
|
-
tmp_path = tmp.name
|
|
188
|
-
try:
|
|
189
|
-
pager_runner(tmp_path)
|
|
190
|
-
return True
|
|
191
|
-
except Exception:
|
|
192
|
-
# If pager fails, return False to indicate paging was not successful
|
|
193
|
-
return False
|
|
194
|
-
finally:
|
|
195
|
-
try:
|
|
196
|
-
os.unlink(tmp_path)
|
|
197
|
-
except Exception:
|
|
198
|
-
pass
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def _page_with_system_pager(
|
|
202
|
-
ansi_text: str,
|
|
203
|
-
) -> bool: # pragma: no cover - spawns real pager
|
|
204
|
-
"""Prefer 'less' with a temp file so stdin remains the TTY."""
|
|
205
|
-
if not _should_use_pager():
|
|
206
|
-
return False
|
|
207
|
-
|
|
208
|
-
pager_cmd, less_path = _resolve_pager_command()
|
|
209
|
-
|
|
210
|
-
if pager_cmd or less_path:
|
|
211
|
-
return _run_pager_with_temp_file(
|
|
212
|
-
lambda tmp_path: _run_less_pager(pager_cmd, less_path, tmp_path), ansi_text
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
# Windows 'more' is poor with ANSI; let Rich fallback handle it
|
|
216
|
-
if platform.system().lower().startswith("win"):
|
|
217
|
-
return False
|
|
218
|
-
|
|
219
|
-
# POSIX 'more' fallback (may or may not honor ANSI)
|
|
220
|
-
return _run_pager_with_temp_file(_run_more_pager, ansi_text)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def _get_view(ctx: Any) -> str:
|
|
224
|
-
view = get_ctx_value(ctx, "view")
|
|
225
|
-
if view:
|
|
226
|
-
return view
|
|
227
|
-
|
|
228
|
-
fallback = get_ctx_value(ctx, "format")
|
|
229
|
-
return fallback or "rich"
|
|
230
|
-
|
|
231
|
-
|
|
232
56
|
def spinner_context(
|
|
233
57
|
ctx: Any | None,
|
|
234
58
|
message: str,
|
|
@@ -347,63 +171,6 @@ def get_client(ctx: Any) -> Client: # pragma: no cover
|
|
|
347
171
|
)
|
|
348
172
|
|
|
349
173
|
|
|
350
|
-
# ----------------------------- Secret masking ---------------------------- #
|
|
351
|
-
|
|
352
|
-
_DEFAULT_MASK_FIELDS = {
|
|
353
|
-
"api_key",
|
|
354
|
-
"apikey",
|
|
355
|
-
"token",
|
|
356
|
-
"access_token",
|
|
357
|
-
"secret",
|
|
358
|
-
"client_secret",
|
|
359
|
-
"password",
|
|
360
|
-
"private_key",
|
|
361
|
-
"bearer",
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def _mask_value(v: Any) -> str:
|
|
366
|
-
s = str(v)
|
|
367
|
-
if len(s) <= 8:
|
|
368
|
-
return "••••"
|
|
369
|
-
return f"{s[:4]}••••••••{s[-4:]}"
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def _mask_any(value: Any, mask_fields: set[str]) -> Any:
|
|
373
|
-
"""Recursively mask sensitive fields in mappings / lists."""
|
|
374
|
-
|
|
375
|
-
if isinstance(value, dict):
|
|
376
|
-
masked: dict[Any, Any] = {}
|
|
377
|
-
for key, raw in value.items():
|
|
378
|
-
if isinstance(key, str) and key.lower() in mask_fields and raw is not None:
|
|
379
|
-
masked[key] = _mask_value(raw)
|
|
380
|
-
else:
|
|
381
|
-
masked[key] = _mask_any(raw, mask_fields)
|
|
382
|
-
return masked
|
|
383
|
-
|
|
384
|
-
if isinstance(value, list):
|
|
385
|
-
return [_mask_any(item, mask_fields) for item in value]
|
|
386
|
-
|
|
387
|
-
return value
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
def _maybe_mask_row(row: dict[str, Any], mask_fields: set[str]) -> dict[str, Any]:
|
|
391
|
-
"""Mask a single row (legacy function, now uses _mask_any)."""
|
|
392
|
-
if not mask_fields:
|
|
393
|
-
return row
|
|
394
|
-
return _mask_any(row, mask_fields)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
def _resolve_mask_fields() -> set[str]:
|
|
398
|
-
if os.getenv("AIP_MASK_OFF", "0") in ("1", "true", "on", "yes"):
|
|
399
|
-
return set()
|
|
400
|
-
env_fields = (os.getenv("AIP_MASK_FIELDS") or "").strip()
|
|
401
|
-
if env_fields:
|
|
402
|
-
parts = [p.strip().lower() for p in env_fields.split(",") if p.strip()]
|
|
403
|
-
return set(parts)
|
|
404
|
-
return set(_DEFAULT_MASK_FIELDS)
|
|
405
|
-
|
|
406
|
-
|
|
407
174
|
# ----------------------------- Fuzzy palette ----------------------------- #
|
|
408
175
|
|
|
409
176
|
|
|
@@ -667,16 +434,6 @@ def _coerce_result_payload(result: Any) -> Any:
|
|
|
667
434
|
return result
|
|
668
435
|
|
|
669
436
|
|
|
670
|
-
def _apply_mask_if_configured(payload: Any) -> Any:
|
|
671
|
-
mask_fields = _resolve_mask_fields()
|
|
672
|
-
if not mask_fields:
|
|
673
|
-
return payload
|
|
674
|
-
try:
|
|
675
|
-
return _mask_any(payload, mask_fields)
|
|
676
|
-
except Exception:
|
|
677
|
-
return payload
|
|
678
|
-
|
|
679
|
-
|
|
680
437
|
def _ensure_displayable(payload: Any) -> Any:
|
|
681
438
|
if isinstance(payload, dict | list | str | int | float | bool) or payload is None:
|
|
682
439
|
return payload
|
|
@@ -713,7 +470,7 @@ def output_result(
|
|
|
713
470
|
fmt = _get_view(ctx)
|
|
714
471
|
|
|
715
472
|
data = _coerce_result_payload(result)
|
|
716
|
-
data =
|
|
473
|
+
data = masking.mask_payload(data)
|
|
717
474
|
data = _ensure_displayable(data)
|
|
718
475
|
|
|
719
476
|
if fmt == "json":
|
|
@@ -771,16 +528,6 @@ def _normalise_rows(
|
|
|
771
528
|
return []
|
|
772
529
|
|
|
773
530
|
|
|
774
|
-
def _mask_rows_if_configured(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
775
|
-
mask_fields = _resolve_mask_fields()
|
|
776
|
-
if not mask_fields:
|
|
777
|
-
return rows
|
|
778
|
-
try:
|
|
779
|
-
return [_maybe_mask_row(row, mask_fields) for row in rows]
|
|
780
|
-
except Exception:
|
|
781
|
-
return rows
|
|
782
|
-
|
|
783
|
-
|
|
784
531
|
def _render_plain_list(
|
|
785
532
|
rows: list[dict[str, Any]], title: str, columns: list[tuple]
|
|
786
533
|
) -> None:
|
|
@@ -832,20 +579,6 @@ def _build_table_group(
|
|
|
832
579
|
return Group(table, footer)
|
|
833
580
|
|
|
834
581
|
|
|
835
|
-
def _should_page_output(row_count: int, is_tty: bool) -> bool:
|
|
836
|
-
pager_env = (os.getenv("AIP_PAGER", "auto") or "auto").lower()
|
|
837
|
-
if pager_env in ("0", "off", "false"):
|
|
838
|
-
return False
|
|
839
|
-
if pager_env in ("1", "on", "true"):
|
|
840
|
-
return is_tty
|
|
841
|
-
try:
|
|
842
|
-
term_h = console.size.height or 24
|
|
843
|
-
approx_lines = 5 + row_count
|
|
844
|
-
return is_tty and (approx_lines >= term_h * 0.5)
|
|
845
|
-
except Exception:
|
|
846
|
-
return is_tty
|
|
847
|
-
|
|
848
|
-
|
|
849
582
|
def _handle_json_output(items: list[Any], rows: list[dict[str, Any]]) -> None:
|
|
850
583
|
"""Handle JSON output format."""
|
|
851
584
|
data = (
|
|
@@ -947,14 +680,14 @@ def _handle_table_output(
|
|
|
947
680
|
"""Handle table output with paging."""
|
|
948
681
|
content = _build_table_group(rows, columns, title)
|
|
949
682
|
should_page = (
|
|
950
|
-
_should_page_output(len(rows), console.is_terminal and os.isatty(1))
|
|
683
|
+
pager._should_page_output(len(rows), console.is_terminal and os.isatty(1))
|
|
951
684
|
if use_pager is None
|
|
952
685
|
else use_pager
|
|
953
686
|
)
|
|
954
687
|
|
|
955
688
|
if should_page:
|
|
956
|
-
ansi = _render_ansi(content)
|
|
957
|
-
if not _page_with_system_pager(ansi):
|
|
689
|
+
ansi = pager._render_ansi(content)
|
|
690
|
+
if not pager._page_with_system_pager(ansi):
|
|
958
691
|
with console.pager(styles=True):
|
|
959
692
|
console.print(content)
|
|
960
693
|
else:
|
|
@@ -974,7 +707,7 @@ def output_list(
|
|
|
974
707
|
"""Display a list with optional fuzzy palette for quick selection."""
|
|
975
708
|
fmt = _get_view(ctx)
|
|
976
709
|
rows = _normalise_rows(items, transform_func)
|
|
977
|
-
rows =
|
|
710
|
+
rows = masking.mask_rows(rows)
|
|
978
711
|
|
|
979
712
|
if fmt == "json":
|
|
980
713
|
_handle_json_output(items, rows)
|
|
@@ -1004,50 +737,6 @@ def output_list(
|
|
|
1004
737
|
_handle_table_output(rows, columns, title, use_pager=use_pager)
|
|
1005
738
|
|
|
1006
739
|
|
|
1007
|
-
# ------------------------- Output flags decorator ------------------------ #
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
def _set_view(ctx: Any, _param: Any, value: str) -> None:
|
|
1011
|
-
if not value:
|
|
1012
|
-
return
|
|
1013
|
-
ctx.ensure_object(dict)
|
|
1014
|
-
ctx.obj["view"] = value
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
def _set_json(ctx: Any, _param: Any, value: bool) -> None:
|
|
1018
|
-
if not value:
|
|
1019
|
-
return
|
|
1020
|
-
ctx.ensure_object(dict)
|
|
1021
|
-
ctx.obj["view"] = "json"
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
def output_flags() -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
1025
|
-
"""Decorator to allow output format flags on any subcommand."""
|
|
1026
|
-
|
|
1027
|
-
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
1028
|
-
f = click.option(
|
|
1029
|
-
"--json",
|
|
1030
|
-
"json_mode",
|
|
1031
|
-
is_flag=True,
|
|
1032
|
-
expose_value=False,
|
|
1033
|
-
help="Shortcut for --view json",
|
|
1034
|
-
callback=_set_json,
|
|
1035
|
-
)(f)
|
|
1036
|
-
f = click.option(
|
|
1037
|
-
"-o",
|
|
1038
|
-
"--output",
|
|
1039
|
-
"--view",
|
|
1040
|
-
"view_opt",
|
|
1041
|
-
type=click.Choice(["rich", "plain", "json", "md"]),
|
|
1042
|
-
expose_value=False,
|
|
1043
|
-
help="Output format",
|
|
1044
|
-
callback=_set_view,
|
|
1045
|
-
)(f)
|
|
1046
|
-
return f
|
|
1047
|
-
|
|
1048
|
-
return decorator
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
740
|
# ------------------------- Ambiguity handling --------------------------- #
|
|
1052
741
|
|
|
1053
742
|
|
|
@@ -1400,19 +1089,3 @@ def handle_ambiguous_resource(
|
|
|
1400
1089
|
else:
|
|
1401
1090
|
# Re-raise cancellation exceptions
|
|
1402
1091
|
raise
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
def detect_export_format(file_path: str | Path) -> str:
|
|
1406
|
-
"""Detect export format from file extension.
|
|
1407
|
-
|
|
1408
|
-
Args:
|
|
1409
|
-
file_path: Path to the export file
|
|
1410
|
-
|
|
1411
|
-
Returns:
|
|
1412
|
-
"yaml" if file extension is .yaml or .yml, "json" otherwise
|
|
1413
|
-
"""
|
|
1414
|
-
path = Path(file_path)
|
|
1415
|
-
if path.suffix.lower() in [".yaml", ".yml"]:
|
|
1416
|
-
return "yaml"
|
|
1417
|
-
else:
|
|
1418
|
-
return "json"
|
|
@@ -5,15 +5,18 @@ glaip_sdk/cli/__init__.py,sha256=xCCfuF1Yc7mpCDcfhHZTX0vizvtrDSLeT8MJ3V7m5A0,156
|
|
|
5
5
|
glaip_sdk/cli/agent_config.py,sha256=VHjebw68wAdhGUzYdPH8qz10oADZPRgUQcPW6F7iHIU,2421
|
|
6
6
|
glaip_sdk/cli/auth.py,sha256=eYdtGmJ3XgiO96hq_69GF6b3W-aRWZrDQ-6bHuaRX4M,13517
|
|
7
7
|
glaip_sdk/cli/commands/__init__.py,sha256=x0CZlZbZHoHvuzfoTWIyEch6WmNnbPzxajrox6riYp0,173
|
|
8
|
-
glaip_sdk/cli/commands/agents.py,sha256=
|
|
8
|
+
glaip_sdk/cli/commands/agents.py,sha256=Ejg_IFXfvajW2q3IrmotiLjljEYknKEcMejdvVCWoDs,40644
|
|
9
9
|
glaip_sdk/cli/commands/configure.py,sha256=eRDzsaKV4fl2lJt8ieS4g2-xRnaa02eAAPW8xBf-tqA,7507
|
|
10
|
-
glaip_sdk/cli/commands/mcps.py,sha256=
|
|
11
|
-
glaip_sdk/cli/commands/models.py,sha256=
|
|
12
|
-
glaip_sdk/cli/commands/tools.py,sha256=
|
|
10
|
+
glaip_sdk/cli/commands/mcps.py,sha256=I-cqVqAGqSiUDgWOEB6eUeKg_wMtteoPyyXwu96qq6Q,28207
|
|
11
|
+
glaip_sdk/cli/commands/models.py,sha256=G1ce-wZOfvMP6SMnIVuSQ89CF444Kz8Ja6nrNOQXCqU,1729
|
|
12
|
+
glaip_sdk/cli/commands/tools.py,sha256=P3zuKVapoC3yV4rnHGdFPO_snjLGWo5IpfuYHIUfMeU,18711
|
|
13
|
+
glaip_sdk/cli/context.py,sha256=M4weRf8dmp5bMtPLRF3w1StnRB7Lo8FPFq2GQMv3Rv8,3617
|
|
13
14
|
glaip_sdk/cli/display.py,sha256=jE20swoRKzpYUmc0jgbeonaXKeE9x95hfjWAEdnBYRc,8727
|
|
14
15
|
glaip_sdk/cli/io.py,sha256=GPkw3pQMLBGoD5GH-KlbKpNRlVWFZOXHE17F7V3kQsI,3343
|
|
15
16
|
glaip_sdk/cli/main.py,sha256=3Bl8u9t1MekzaNrAZqsx4TukbzzFdi6Wss6jvTDos00,12930
|
|
17
|
+
glaip_sdk/cli/masking.py,sha256=BOZjwUqxQf3LQlYgUMwq7UYgve8x4_1Qk04ixiJJPZ8,4399
|
|
16
18
|
glaip_sdk/cli/mcp_validators.py,sha256=PEJRzb7ogRkwNJwJK9k5Xmb8hvoQ58L2Qywqd_3Wayo,10125
|
|
19
|
+
glaip_sdk/cli/pager.py,sha256=KmOBhY66JHg2vRpiNJ69RnZiF8sFer7yR8NIdlxnALk,8007
|
|
17
20
|
glaip_sdk/cli/parsers/__init__.py,sha256=Ycd4HDfYmA7GUGFt0ndBPBo5uTbv15XsXnYUj-a89ug,183
|
|
18
21
|
glaip_sdk/cli/parsers/json_input.py,sha256=iISa31ZsDNYWfCVRy0cifRIg2gjnhI-XtdDLB-UOshg,4039
|
|
19
22
|
glaip_sdk/cli/resolution.py,sha256=BOw2NchReLKewAwBAZLWw_3_bI7u3tfzQEO7kQbIiGE,2067
|
|
@@ -22,7 +25,7 @@ glaip_sdk/cli/slash/agent_session.py,sha256=pDOwGXNHuyJIulrGYu1pacyF3oxHWeDQY-Uv
|
|
|
22
25
|
glaip_sdk/cli/slash/prompt.py,sha256=Pr5SSTOKFssRsi-AujOm5_BCW_f5MxgLwJ3CCji1ogM,7356
|
|
23
26
|
glaip_sdk/cli/slash/session.py,sha256=U5UEL6eIvNkIJcSz04Uf8Ql0EptmLJukqHxDCAJ-nOQ,31097
|
|
24
27
|
glaip_sdk/cli/update_notifier.py,sha256=uVbjZJnW4znTzx4AkqsDO4NfXiF-mtQiypTkJByAVuM,3236
|
|
25
|
-
glaip_sdk/cli/utils.py,sha256=
|
|
28
|
+
glaip_sdk/cli/utils.py,sha256=IG1h0wY6rIYvDRcTZuOmSgAHiYlx6yD9SKHrTbDFHmc,33077
|
|
26
29
|
glaip_sdk/cli/validators.py,sha256=USbBgY86AwuDHO-Q_g8g7hu-ot4NgITBsWjTWIl62ms,5569
|
|
27
30
|
glaip_sdk/client/__init__.py,sha256=nYLXfBVTTWwKjP0e63iumPYO4k5FifwWaELQPaPIKIg,188
|
|
28
31
|
glaip_sdk/client/agents.py,sha256=FSKubF40wptMNIheC3_iawiX2CRbhTcNLFiz4qkPC6k,34659
|
|
@@ -58,7 +61,7 @@ glaip_sdk/utils/rich_utils.py,sha256=-Ij-1bIJvnVAi6DrfftchIlMcvOTjVmSE0Qqax0EY_s
|
|
|
58
61
|
glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
|
|
59
62
|
glaip_sdk/utils/serialization.py,sha256=T1yt_8G2DCFpcxx7XnqFl5slksRXfBCUuLQJTreGYEQ,11806
|
|
60
63
|
glaip_sdk/utils/validation.py,sha256=QNORcdyvuliEs4EH2_mkDgmoyT9utgl7YNhaf45SEf8,6992
|
|
61
|
-
glaip_sdk-0.0.
|
|
62
|
-
glaip_sdk-0.0.
|
|
63
|
-
glaip_sdk-0.0.
|
|
64
|
-
glaip_sdk-0.0.
|
|
64
|
+
glaip_sdk-0.0.15.dist-info/METADATA,sha256=Hp6gkY7Ci5eTGCScvyovsLl2j0g7Dlw1kQ3chFWYwrI,5168
|
|
65
|
+
glaip_sdk-0.0.15.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
66
|
+
glaip_sdk-0.0.15.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
|
|
67
|
+
glaip_sdk-0.0.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|