acex-cli 4.5.0__tar.gz → 4.5.1__tar.gz
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.
- {acex_cli-4.5.0 → acex_cli-4.5.1}/PKG-INFO +4 -4
- {acex_cli-4.5.0 → acex_cli-4.5.1}/pyproject.toml +8 -4
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/commands/asset.py +16 -14
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/commands/context.py +7 -4
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/commands/logical_node.py +14 -13
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/commands/ned.py +4 -6
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/commands/node.py +57 -36
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/context.py +1 -0
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/diff_formatters.py +77 -75
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/main.py +7 -7
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/output.py +9 -6
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/sdk.py +0 -3
- {acex_cli-4.5.0 → acex_cli-4.5.1}/README.md +0 -0
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/__init__.py +0 -0
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/commands/__init__.py +0 -0
- {acex_cli-4.5.0 → acex_cli-4.5.1}/src/acex_cli/print_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: acex-cli
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.1
|
|
4
4
|
Summary: ACE-X CLI - Command-line interface for ACE-X
|
|
5
5
|
License: AGPL-3.0
|
|
6
6
|
Keywords: automation,cli,control
|
|
@@ -11,9 +11,9 @@ Classifier: License :: OSI Approved :: GNU Affero General Public License v3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
-
Requires-Dist: acex-client (>=4.
|
|
15
|
-
Requires-Dist: acex-devkit (>=
|
|
16
|
-
Requires-Dist: acex-driver-cisco-ioscli (>=
|
|
14
|
+
Requires-Dist: acex-client (>=4.8.1,<5.0.0)
|
|
15
|
+
Requires-Dist: acex-devkit (>=2.4.0,<3.0.0)
|
|
16
|
+
Requires-Dist: acex-driver-cisco-ioscli (>=2.7.0,<3.0.0)
|
|
17
17
|
Requires-Dist: click (==8.1.7)
|
|
18
18
|
Requires-Dist: ntc-templates (>=8.1.0,<9.0.0)
|
|
19
19
|
Requires-Dist: rich (>=13.0.0,<14.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "acex-cli"
|
|
3
|
-
version = "4.5.
|
|
3
|
+
version = "4.5.1"
|
|
4
4
|
description = "ACE-X CLI - Command-line interface for ACE-X"
|
|
5
5
|
authors = ["Johan Lahti <johan.lahti@acebit.se>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -17,14 +17,18 @@ python = "^3.13"
|
|
|
17
17
|
typer = "^0.12.0"
|
|
18
18
|
rich = "^13.0.0"
|
|
19
19
|
click = "8.1.7"
|
|
20
|
-
acex-client = "^4.
|
|
21
|
-
acex-devkit = "^
|
|
22
|
-
acex-driver-cisco-ioscli = "^
|
|
20
|
+
acex-client = "^4.8.1" # Changes to real dep. by switch_acex_client_dep for publishing
|
|
21
|
+
acex-devkit = "^2.4.0"
|
|
22
|
+
acex-driver-cisco-ioscli = "^2.7.0"
|
|
23
23
|
ntc-templates = "^8.1.0"
|
|
24
24
|
|
|
25
25
|
[tool.poetry.scripts]
|
|
26
26
|
acex = "acex_cli.main:app"
|
|
27
27
|
|
|
28
|
+
[tool.poetry.group.dev.dependencies]
|
|
29
|
+
pytest = "^8.0.0"
|
|
30
|
+
pytest-asyncio = "^0.23.0"
|
|
31
|
+
|
|
28
32
|
[build-system]
|
|
29
33
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
30
34
|
build-backend = "poetry.core.masonry.api"
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
import typer
|
|
2
|
+
from acex_client.models.generated_models import AssetResponse
|
|
4
3
|
|
|
5
|
-
from acex_cli.sdk import get_sdk
|
|
6
4
|
from acex_cli.output import display_list, display_object
|
|
7
|
-
from
|
|
5
|
+
from acex_cli.sdk import get_sdk
|
|
8
6
|
|
|
9
7
|
app = typer.Typer(help="Asset resource commands")
|
|
10
8
|
|
|
@@ -13,25 +11,29 @@ app = typer.Typer(help="Asset resource commands")
|
|
|
13
11
|
def list_cmd(
|
|
14
12
|
ctx: typer.Context,
|
|
15
13
|
# Filters (match backend query params)
|
|
16
|
-
vendor:
|
|
17
|
-
os:
|
|
18
|
-
hardware_model:
|
|
19
|
-
ned_id:
|
|
20
|
-
serial_number:
|
|
21
|
-
assigned:
|
|
14
|
+
vendor: str | None = typer.Option(None, help="Filter by vendor (prefix match)"),
|
|
15
|
+
os: str | None = typer.Option(None, "--os", help="Filter by OS"),
|
|
16
|
+
hardware_model: str | None = typer.Option(None, help="Filter by hardware model"),
|
|
17
|
+
ned_id: str | None = typer.Option(None, help="Filter by NED ID"),
|
|
18
|
+
serial_number: str | None = typer.Option(None, help="Filter by serial number"),
|
|
19
|
+
assigned: bool | None = typer.Option(None, help="Filter assigned/unassigned"),
|
|
22
20
|
# Pagination
|
|
23
21
|
limit: int = typer.Option(100, "--limit", "-l", help="Max items to return"),
|
|
24
22
|
offset: int = typer.Option(0, "--offset", help="Items to skip"),
|
|
25
23
|
# Output
|
|
26
24
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
27
|
-
columns:
|
|
25
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated columns"),
|
|
28
26
|
no_header: bool = typer.Option(False, "--no-header", help="Hide table header"),
|
|
29
27
|
):
|
|
30
28
|
"""List assets with optional filters."""
|
|
31
29
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
32
30
|
filters = _compact(
|
|
33
|
-
vendor=vendor,
|
|
34
|
-
|
|
31
|
+
vendor=vendor,
|
|
32
|
+
os=os,
|
|
33
|
+
hardware_model=hardware_model,
|
|
34
|
+
ned_id=ned_id,
|
|
35
|
+
serial_number=serial_number,
|
|
36
|
+
assigned=assigned,
|
|
35
37
|
)
|
|
36
38
|
result = sdk.assets.query(limit=limit, offset=offset, **filters)
|
|
37
39
|
display_list(
|
|
@@ -49,7 +51,7 @@ def show_cmd(
|
|
|
49
51
|
ctx: typer.Context,
|
|
50
52
|
asset_id: str,
|
|
51
53
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
52
|
-
columns:
|
|
54
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
|
|
53
55
|
):
|
|
54
56
|
"""Show details for an asset."""
|
|
55
57
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
from acex_cli.context import CLIContext
|
|
3
2
|
|
|
4
3
|
app = typer.Typer(help="Context management. Configure API url etc.")
|
|
5
4
|
|
|
5
|
+
|
|
6
6
|
@app.command("verify-ssl")
|
|
7
7
|
def verify_ssl(
|
|
8
8
|
value: str = typer.Argument(..., help="Set verify_ssl to true or false", show_default=False, case_sensitive=False),
|
|
9
|
-
ctx: typer.Context = typer.Context
|
|
9
|
+
ctx: typer.Context = typer.Context,
|
|
10
10
|
):
|
|
11
11
|
"""Update verify_ssl for the active context. Usage: acex context verify-ssl true|false"""
|
|
12
12
|
value = value.lower()
|
|
@@ -23,17 +23,19 @@ def verify_ssl(
|
|
|
23
23
|
ctx.obj.save()
|
|
24
24
|
typer.echo(f"verify_ssl for active context '{active}' set to {bool_value}.")
|
|
25
25
|
|
|
26
|
+
|
|
26
27
|
@app.command()
|
|
27
28
|
def add(
|
|
28
29
|
name: str = typer.Option(..., "--name", help="Context name"),
|
|
29
30
|
url: str = typer.Option(..., "--url", help="API URL"),
|
|
30
31
|
verify_ssl: bool = typer.Option(True, "--verify-ssl/--no-verify-ssl", help="Verify SSL certificates"),
|
|
31
|
-
ctx: typer.Context = typer.Context
|
|
32
|
+
ctx: typer.Context = typer.Context,
|
|
32
33
|
):
|
|
33
34
|
"""Set or update a context."""
|
|
34
35
|
ctx.obj.set_context(name, url, verify_ssl=verify_ssl)
|
|
35
36
|
typer.echo(f"Context '{name}' set to {url} (verify_ssl={verify_ssl}) and activated.")
|
|
36
37
|
|
|
38
|
+
|
|
37
39
|
@app.command()
|
|
38
40
|
def list(ctx: typer.Context):
|
|
39
41
|
"""List all saved contexts."""
|
|
@@ -47,12 +49,14 @@ def list(ctx: typer.Context):
|
|
|
47
49
|
url = data.get("url", "")
|
|
48
50
|
typer.echo(f"{marker} {name}: {url}")
|
|
49
51
|
|
|
52
|
+
|
|
50
53
|
@app.command()
|
|
51
54
|
def use(name: str, ctx: typer.Context):
|
|
52
55
|
"""Set active context by name."""
|
|
53
56
|
ctx.obj.set_active(name)
|
|
54
57
|
typer.echo(f"Active context set to '{name}'")
|
|
55
58
|
|
|
59
|
+
|
|
56
60
|
@app.command()
|
|
57
61
|
def show(ctx: typer.Context):
|
|
58
62
|
"""Show active context details."""
|
|
@@ -61,4 +65,3 @@ def show(ctx: typer.Context):
|
|
|
61
65
|
typer.echo(f"Active context: {context}")
|
|
62
66
|
else:
|
|
63
67
|
typer.echo("No active context set.")
|
|
64
|
-
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
import typer
|
|
2
|
+
from acex_client.models.generated_models import LogicalNode
|
|
4
3
|
|
|
5
|
-
from acex_cli.sdk import get_sdk
|
|
6
4
|
from acex_cli.output import display_list, display_object
|
|
7
|
-
from
|
|
5
|
+
from acex_cli.sdk import get_sdk
|
|
8
6
|
|
|
9
7
|
app = typer.Typer(help="Logical node resource commands")
|
|
10
8
|
|
|
@@ -13,24 +11,27 @@ app = typer.Typer(help="Logical node resource commands")
|
|
|
13
11
|
def list_cmd(
|
|
14
12
|
ctx: typer.Context,
|
|
15
13
|
# Filters (match backend query params)
|
|
16
|
-
role:
|
|
17
|
-
site:
|
|
18
|
-
hostname:
|
|
19
|
-
sequence:
|
|
20
|
-
assigned:
|
|
14
|
+
role: str | None = typer.Option(None, help="Filter by role (prefix match)"),
|
|
15
|
+
site: str | None = typer.Option(None, help="Filter by site (prefix match)"),
|
|
16
|
+
hostname: str | None = typer.Option(None, help="Filter by hostname (prefix match)"),
|
|
17
|
+
sequence: int | None = typer.Option(None, help="Filter by sequence number"),
|
|
18
|
+
assigned: bool | None = typer.Option(None, help="Filter assigned/unassigned"),
|
|
21
19
|
# Pagination
|
|
22
20
|
limit: int = typer.Option(100, "--limit", "-l", help="Max items to return"),
|
|
23
21
|
offset: int = typer.Option(0, "--offset", help="Items to skip"),
|
|
24
22
|
# Output
|
|
25
23
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
26
|
-
columns:
|
|
24
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated columns"),
|
|
27
25
|
no_header: bool = typer.Option(False, "--no-header", help="Hide table header"),
|
|
28
26
|
):
|
|
29
27
|
"""List logical nodes with optional filters."""
|
|
30
28
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
31
29
|
filters = _compact(
|
|
32
|
-
role=role,
|
|
33
|
-
|
|
30
|
+
role=role,
|
|
31
|
+
site=site,
|
|
32
|
+
hostname=hostname,
|
|
33
|
+
sequence=sequence,
|
|
34
|
+
assigned=assigned,
|
|
34
35
|
)
|
|
35
36
|
result = sdk.logical_nodes.query(limit=limit, offset=offset, **filters)
|
|
36
37
|
display_list(
|
|
@@ -48,7 +49,7 @@ def show_cmd(
|
|
|
48
49
|
ctx: typer.Context,
|
|
49
50
|
logical_node_id: str,
|
|
50
51
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
51
|
-
columns:
|
|
52
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
|
|
52
53
|
):
|
|
53
54
|
"""Show details for a logical node."""
|
|
54
55
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
1
|
import typer
|
|
2
|
+
from acex_client.models.generated_models import Ned
|
|
4
3
|
|
|
5
|
-
from acex_cli.sdk import get_sdk
|
|
6
4
|
from acex_cli.output import display_list, display_object
|
|
7
|
-
from
|
|
5
|
+
from acex_cli.sdk import get_sdk
|
|
8
6
|
|
|
9
7
|
app = typer.Typer(help="NED (Network Element Driver) commands")
|
|
10
8
|
|
|
@@ -14,7 +12,7 @@ def list_cmd(
|
|
|
14
12
|
ctx: typer.Context,
|
|
15
13
|
# Output (NEDs endpoint has no server-side filters or pagination)
|
|
16
14
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
17
|
-
columns:
|
|
15
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated columns"),
|
|
18
16
|
no_header: bool = typer.Option(False, "--no-header", help="Hide table header"),
|
|
19
17
|
):
|
|
20
18
|
"""List available NEDs."""
|
|
@@ -35,7 +33,7 @@ def show_cmd(
|
|
|
35
33
|
ctx: typer.Context,
|
|
36
34
|
ned_id: str,
|
|
37
35
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
38
|
-
columns:
|
|
36
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
|
|
39
37
|
):
|
|
40
38
|
"""Show details for a NED."""
|
|
41
39
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import requests
|
|
7
6
|
import typer
|
|
8
|
-
|
|
9
|
-
from acex_cli.sdk import get_sdk
|
|
10
|
-
from acex_cli.output import display_list, display_object
|
|
11
|
-
from acex_cli.print_utils import print_commands_box
|
|
12
7
|
from acex_devkit.models import NodeListItem
|
|
13
8
|
from acex_devkit.models.composed_configuration import ComposedConfiguration
|
|
9
|
+
|
|
14
10
|
from acex_cli.diff_formatters import (
|
|
15
|
-
print_diff_summary,
|
|
16
|
-
print_diff_tree,
|
|
17
11
|
print_diff_compact,
|
|
18
12
|
print_diff_flat,
|
|
13
|
+
print_diff_summary,
|
|
14
|
+
print_diff_tree,
|
|
19
15
|
)
|
|
20
|
-
|
|
16
|
+
from acex_cli.output import display_list, display_object
|
|
17
|
+
from acex_cli.print_utils import print_commands_box
|
|
18
|
+
from acex_cli.sdk import get_sdk
|
|
21
19
|
|
|
22
20
|
# ── Temporary REST helpers (replace with SDK methods) ───────────
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
def _request_get(sdk, path: str, params: dict | None = None) -> requests.Response:
|
|
25
24
|
verify = getattr(getattr(sdk, "rest", None), "verify", True)
|
|
26
25
|
url = f"{sdk.api_url}{path}"
|
|
27
26
|
response = requests.get(url, params=params, verify=verify)
|
|
@@ -30,7 +29,7 @@ def _request_get(sdk, path: str, params: Optional[dict] = None) -> requests.Resp
|
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
def _get_connection(sdk, node_id):
|
|
33
|
-
return _request_get(sdk, f"/inventory/management_connections
|
|
32
|
+
return _request_get(sdk, f"/inventory/management_connections?node_id={node_id}").json()
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
def _get_rendered_config(sdk, node_id: str) -> str:
|
|
@@ -57,6 +56,7 @@ def _fetch_node_instance(sdk, identifier: str):
|
|
|
57
56
|
if len(result) > 1:
|
|
58
57
|
typer.echo(f"Multiple nodes match '{identifier}':")
|
|
59
58
|
from acex_cli.output import display_list
|
|
59
|
+
|
|
60
60
|
display_list(result, columns=["id", "hostname", "site", "status"], title="")
|
|
61
61
|
raise typer.Exit(1)
|
|
62
62
|
node = sdk.node_instances.get(result.items[0].id)
|
|
@@ -83,6 +83,7 @@ def _resolve_node_quick(sdk, identifier: str):
|
|
|
83
83
|
if len(result) > 1:
|
|
84
84
|
typer.echo(f"Multiple nodes match '{identifier}':")
|
|
85
85
|
from acex_cli.output import display_list
|
|
86
|
+
|
|
86
87
|
display_list(result, columns=["id", "hostname", "site", "status"], title="")
|
|
87
88
|
raise typer.Exit(1)
|
|
88
89
|
return result.items[0]
|
|
@@ -90,17 +91,17 @@ def _resolve_node_quick(sdk, identifier: str):
|
|
|
90
91
|
|
|
91
92
|
def _compile_local(sdk, identifier: str, config_maps_dir: str):
|
|
92
93
|
"""Compile config locally from config maps on disk. Returns ComposedConfiguration."""
|
|
93
|
-
import os
|
|
94
94
|
import importlib.util
|
|
95
|
+
import os
|
|
95
96
|
import sys
|
|
96
97
|
|
|
97
98
|
try:
|
|
98
99
|
from acex.config_map import ConfigMap
|
|
99
|
-
from acex.configuration import Configuration
|
|
100
100
|
from acex.config_map.context import ConfigMapContext
|
|
101
|
-
|
|
101
|
+
from acex.configuration import Configuration
|
|
102
|
+
except ImportError as exc:
|
|
102
103
|
typer.echo("Backend package 'acex' not installed. Run: pip install -e ../backend")
|
|
103
|
-
raise typer.Exit(1)
|
|
104
|
+
raise typer.Exit(1) from exc
|
|
104
105
|
|
|
105
106
|
if not os.path.isdir(config_maps_dir):
|
|
106
107
|
typer.echo(f"Directory not found: {config_maps_dir}")
|
|
@@ -195,28 +196,32 @@ config_app.add_typer(config_diff_app, name="diff")
|
|
|
195
196
|
|
|
196
197
|
# ── Node list / show ────────────────────────────────────────────
|
|
197
198
|
|
|
199
|
+
|
|
198
200
|
@app.command("list")
|
|
199
201
|
def list_cmd(
|
|
200
202
|
ctx: typer.Context,
|
|
201
203
|
# Filters (match backend query params)
|
|
202
|
-
site:
|
|
203
|
-
hostname:
|
|
204
|
-
logical_node_id:
|
|
205
|
-
asset_ref_id:
|
|
206
|
-
status:
|
|
204
|
+
site: str | None = typer.Option(None, help="Filter by site (prefix match)"),
|
|
205
|
+
hostname: str | None = typer.Option(None, help="Filter by hostname (prefix match)"),
|
|
206
|
+
logical_node_id: int | None = typer.Option(None, help="Filter by logical node ID"),
|
|
207
|
+
asset_ref_id: int | None = typer.Option(None, help="Filter by asset ref ID"),
|
|
208
|
+
status: str | None = typer.Option(None, help="Filter by status (planned, init, active, decommissioned)"),
|
|
207
209
|
# Pagination
|
|
208
210
|
limit: int = typer.Option(100, "--limit", "-l", help="Max items to return"),
|
|
209
211
|
offset: int = typer.Option(0, "--offset", help="Items to skip"),
|
|
210
212
|
# Output
|
|
211
213
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
212
|
-
columns:
|
|
214
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated columns"),
|
|
213
215
|
no_header: bool = typer.Option(False, "--no-header", help="Hide table header"),
|
|
214
216
|
):
|
|
215
217
|
"""List node instances with optional filters."""
|
|
216
218
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
217
219
|
filters = _compact(
|
|
218
|
-
site=site,
|
|
219
|
-
|
|
220
|
+
site=site,
|
|
221
|
+
hostname=hostname,
|
|
222
|
+
logical_node_id=logical_node_id,
|
|
223
|
+
asset_ref_id=asset_ref_id,
|
|
224
|
+
status=status,
|
|
220
225
|
)
|
|
221
226
|
result = sdk.node_instances.query(limit=limit, offset=offset, **filters)
|
|
222
227
|
display_list(
|
|
@@ -234,7 +239,7 @@ def show_cmd(
|
|
|
234
239
|
ctx: typer.Context,
|
|
235
240
|
node: str = typer.Argument(help="Node ID or hostname"),
|
|
236
241
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
|
|
237
|
-
columns:
|
|
242
|
+
columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
|
|
238
243
|
):
|
|
239
244
|
"""Show details for a node instance (by ID or hostname)."""
|
|
240
245
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
@@ -277,7 +282,7 @@ def connect_cmd(
|
|
|
277
282
|
connections = _get_connection(sdk, node_id)
|
|
278
283
|
except Exception as e:
|
|
279
284
|
typer.echo(f"Failed to fetch management connection: {e}")
|
|
280
|
-
raise typer.Exit(1)
|
|
285
|
+
raise typer.Exit(1) from e
|
|
281
286
|
|
|
282
287
|
if not connections:
|
|
283
288
|
typer.echo(f"No management connection found for node '{node}'.")
|
|
@@ -304,13 +309,14 @@ def connect_cmd(
|
|
|
304
309
|
|
|
305
310
|
# ── Config show commands ────────────────────────────────────────
|
|
306
311
|
|
|
312
|
+
|
|
307
313
|
@config_show_app.command("local")
|
|
308
314
|
def config_show_local(
|
|
309
315
|
ctx: typer.Context,
|
|
310
316
|
node: str = typer.Argument(help="Node ID or hostname"),
|
|
311
317
|
dir: str = typer.Option(..., "--dir", "-d", help="Path to local config_maps directory"),
|
|
312
318
|
render: bool = typer.Option(False, "--render", help="Show rendered device config"),
|
|
313
|
-
path:
|
|
319
|
+
path: str | None = typer.Option(None, "--path", "-p", help="Drill into section"),
|
|
314
320
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
|
|
315
321
|
):
|
|
316
322
|
"""Compile config locally from config maps on disk."""
|
|
@@ -330,7 +336,7 @@ def config_show_desired(
|
|
|
330
336
|
ctx: typer.Context,
|
|
331
337
|
node: str = typer.Argument(help="Node ID or hostname"),
|
|
332
338
|
render: bool = typer.Option(False, "--render", help="Show rendered device config"),
|
|
333
|
-
path:
|
|
339
|
+
path: str | None = typer.Option(None, "--path", "-p", help="Drill into section, e.g. system.ntp, interfaces"),
|
|
334
340
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
|
|
335
341
|
):
|
|
336
342
|
"""Show desired config."""
|
|
@@ -352,7 +358,7 @@ def config_show_observed(
|
|
|
352
358
|
ctx: typer.Context,
|
|
353
359
|
node: str = typer.Argument(help="Node ID or hostname"),
|
|
354
360
|
render: bool = typer.Option(False, "--render", help="Show rendered device config"),
|
|
355
|
-
path:
|
|
361
|
+
path: str | None = typer.Option(None, "--path", "-p", help="Drill into section, e.g. system.ntp, interfaces"),
|
|
356
362
|
format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
|
|
357
363
|
):
|
|
358
364
|
"""Show observed (backup) config."""
|
|
@@ -378,21 +384,24 @@ def config_show_observed(
|
|
|
378
384
|
|
|
379
385
|
# ── Config diff commands ────────────────────────────────────────
|
|
380
386
|
|
|
387
|
+
|
|
381
388
|
@config_diff_app.command("plan")
|
|
382
389
|
def config_diff_plan(
|
|
383
390
|
ctx: typer.Context,
|
|
384
391
|
node: str = typer.Argument(help="Node ID or hostname"),
|
|
385
392
|
format: str = typer.Option(
|
|
386
|
-
"tree",
|
|
393
|
+
"tree",
|
|
394
|
+
"--format",
|
|
395
|
+
"-f",
|
|
387
396
|
help="Output format: tree, compact, flat, summary, json, commands",
|
|
388
397
|
),
|
|
389
|
-
max_depth:
|
|
398
|
+
max_depth: int | None = typer.Option(None, "--max-depth", "-d", help="Max depth for tree view"),
|
|
390
399
|
no_values: bool = typer.Option(False, "--no-values", help="Hide values in tree view"),
|
|
391
400
|
):
|
|
392
401
|
"""Show planned diff between observed and desired config."""
|
|
393
402
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
394
403
|
node_instance, desired_config = _fetch_desired_config(sdk, node)
|
|
395
|
-
node_id = str(node_instance.id) if hasattr(node_instance,
|
|
404
|
+
node_id = str(node_instance.id) if hasattr(node_instance, "id") and node_instance.id else node
|
|
396
405
|
|
|
397
406
|
observed_raw = _get_latest_observed_config(sdk, node_id, "parsed").get("content")
|
|
398
407
|
observed_config = ComposedConfiguration(**observed_raw)
|
|
@@ -425,7 +434,7 @@ def config_diff_apply(
|
|
|
425
434
|
"""Compute diff and apply config patch to device."""
|
|
426
435
|
sdk = get_sdk(ctx.obj.get_active_context())
|
|
427
436
|
node_instance, desired_config = _fetch_desired_config(sdk, node)
|
|
428
|
-
node_id = str(node_instance.id) if hasattr(node_instance,
|
|
437
|
+
node_id = str(node_instance.id) if hasattr(node_instance, "id") and node_instance.id else node
|
|
429
438
|
|
|
430
439
|
observed_raw = _get_latest_observed_config(sdk, node_id, "parsed").get("content")
|
|
431
440
|
observed_config = ComposedConfiguration(**observed_raw)
|
|
@@ -445,11 +454,13 @@ def config_diff_apply(
|
|
|
445
454
|
|
|
446
455
|
# ── Config display helpers ──────────────────────────────────────
|
|
447
456
|
|
|
457
|
+
|
|
448
458
|
def _print_rendered(text: str):
|
|
449
459
|
"""Print rendered device config with syntax highlighting."""
|
|
450
460
|
from rich.console import Console
|
|
451
|
-
from rich.syntax import Syntax
|
|
452
461
|
from rich.panel import Panel
|
|
462
|
+
from rich.syntax import Syntax
|
|
463
|
+
|
|
453
464
|
console = Console()
|
|
454
465
|
syntax = Syntax(text, "cisco", theme="monokai", line_numbers=True, word_wrap=True)
|
|
455
466
|
console.print(Panel(syntax, border_style="dim"))
|
|
@@ -506,10 +517,11 @@ def _count_components(data) -> int:
|
|
|
506
517
|
return count
|
|
507
518
|
|
|
508
519
|
|
|
509
|
-
def _print_parsed_config(data: dict, path:
|
|
520
|
+
def _print_parsed_config(data: dict, path: str | None, format: str, title: str):
|
|
510
521
|
"""Display parsed config with AttributeValue awareness."""
|
|
511
522
|
from rich.console import Console
|
|
512
523
|
from rich.table import Table
|
|
524
|
+
|
|
513
525
|
console = Console()
|
|
514
526
|
|
|
515
527
|
if format == "json":
|
|
@@ -527,7 +539,11 @@ def _print_parsed_config(data: dict, path: Optional[str], format: str, title: st
|
|
|
527
539
|
console.print(f" [dim]source: {source}[/dim]")
|
|
528
540
|
return
|
|
529
541
|
# Dict of AttributeValues (e.g. system.config)
|
|
530
|
-
if
|
|
542
|
+
if (
|
|
543
|
+
isinstance(section, dict)
|
|
544
|
+
and section
|
|
545
|
+
and all(_is_attribute_value(v) for v in section.values() if isinstance(v, dict) and v)
|
|
546
|
+
):
|
|
531
547
|
_print_attributes_table(console, section, title=path)
|
|
532
548
|
return
|
|
533
549
|
# Container with named entries (e.g. interfaces, ntp.servers)
|
|
@@ -553,12 +569,15 @@ def _print_parsed_config(data: dict, path: Optional[str], format: str, title: st
|
|
|
553
569
|
)
|
|
554
570
|
|
|
555
571
|
console.print(table)
|
|
556
|
-
console.print(
|
|
572
|
+
console.print(
|
|
573
|
+
"[dim]Use --path <section> to drill in, --format json for full dump, --render for device config[/dim]"
|
|
574
|
+
)
|
|
557
575
|
|
|
558
576
|
|
|
559
577
|
def _print_attributes_table(console, data: dict, title: str = ""):
|
|
560
578
|
"""Print a dict where values are AttributeValues as a clean table."""
|
|
561
579
|
from rich.table import Table
|
|
580
|
+
|
|
562
581
|
table = Table(title=title, show_header=True, title_style="bold", title_justify="left")
|
|
563
582
|
table.add_column("Attribute", style="cyan")
|
|
564
583
|
table.add_column("Value", style="white")
|
|
@@ -578,7 +597,6 @@ def _print_attributes_table(console, data: dict, title: str = ""):
|
|
|
578
597
|
|
|
579
598
|
def _print_config_section(console, data: dict, path: str):
|
|
580
599
|
"""Print a config section, handling containers and nested AttributeValues."""
|
|
581
|
-
from rich.table import Table
|
|
582
600
|
|
|
583
601
|
# Check if entries are named components (e.g. interfaces, ntp.servers)
|
|
584
602
|
for entry_name, entry_data in data.items():
|
|
@@ -611,6 +629,7 @@ def _print_config_section(console, data: dict, path: str):
|
|
|
611
629
|
|
|
612
630
|
# ── Util ────────────────────────────────────────────────────────
|
|
613
631
|
|
|
632
|
+
|
|
614
633
|
def _flatten_node(node, node_id: str = None) -> dict:
|
|
615
634
|
"""Flatten NodeResponse into a single-level dict for display."""
|
|
616
635
|
data = node.model_dump() if hasattr(node, "model_dump") else dict(node)
|
|
@@ -653,6 +672,7 @@ def _resolve_node(sdk, identifier: str):
|
|
|
653
672
|
elif len(result) > 1:
|
|
654
673
|
typer.echo(f"Multiple nodes match hostname '{identifier}':")
|
|
655
674
|
from acex_cli.output import display_list
|
|
675
|
+
|
|
656
676
|
display_list(result, columns=["id", "hostname", "site", "status"], title="")
|
|
657
677
|
typer.echo("Use the ID to specify which node.")
|
|
658
678
|
raise typer.Exit(1)
|
|
@@ -672,6 +692,7 @@ def _resolve_node_id(sdk, identifier: str) -> str:
|
|
|
672
692
|
elif len(result) > 1:
|
|
673
693
|
typer.echo(f"Multiple nodes match hostname '{identifier}':")
|
|
674
694
|
from acex_cli.output import display_list
|
|
695
|
+
|
|
675
696
|
display_list(result, columns=["id", "hostname", "site", "status"], title="")
|
|
676
697
|
typer.echo("Use the ID to specify which node.")
|
|
677
698
|
raise typer.Exit(1)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"""Formatters for displaying configuration diffs in CLI."""
|
|
2
|
+
|
|
2
3
|
import json
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
|
|
5
|
+
from acex_devkit.configdiffer import ComponentDiffOp, Diff
|
|
6
|
+
from pydantic import BaseModel
|
|
6
7
|
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.tree import Tree
|
|
7
11
|
|
|
8
12
|
console = Console()
|
|
9
13
|
|
|
10
|
-
from pydantic import BaseModel
|
|
11
|
-
from acex_devkit.configdiffer import Diff, ComponentDiffOp
|
|
12
|
-
|
|
13
14
|
|
|
14
15
|
def _as_dict(obj) -> dict:
|
|
15
16
|
"""Convert a Pydantic model or dict to a plain dict for iteration."""
|
|
@@ -22,54 +23,54 @@ def _as_dict(obj) -> dict:
|
|
|
22
23
|
|
|
23
24
|
def print_diff_summary(diff):
|
|
24
25
|
"""Print a summary of the diff statistics."""
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
if not isinstance(diff, Diff):
|
|
27
28
|
console.print("[red]Invalid diff object[/red]")
|
|
28
29
|
return
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
if diff.is_empty():
|
|
31
32
|
console.print("[green]✓ No changes detected[/green]")
|
|
32
33
|
return
|
|
33
|
-
|
|
34
|
+
|
|
34
35
|
summary = diff.summary()
|
|
35
|
-
|
|
36
|
+
|
|
36
37
|
table = Table(title="Configuration Diff Summary")
|
|
37
38
|
table.add_column("Operation", style="bold")
|
|
38
39
|
table.add_column("Count", style="cyan", justify="right")
|
|
39
|
-
|
|
40
|
+
|
|
40
41
|
if summary.get("added", 0) > 0:
|
|
41
42
|
table.add_row("Added", f"[green]{summary['added']}[/green]")
|
|
42
43
|
if summary.get("removed", 0) > 0:
|
|
43
44
|
table.add_row("Removed", f"[red]{summary['removed']}[/red]")
|
|
44
45
|
if summary.get("changed", 0) > 0:
|
|
45
46
|
table.add_row("Changed", f"[yellow]{summary['changed']}[/yellow]")
|
|
46
|
-
|
|
47
|
+
|
|
47
48
|
console.print(table)
|
|
48
49
|
|
|
49
50
|
|
|
50
51
|
def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
51
52
|
"""
|
|
52
53
|
Print diff as a hierarchical tree view.
|
|
53
|
-
|
|
54
|
+
|
|
54
55
|
Args:
|
|
55
56
|
diff: Diff object from configdiffer
|
|
56
57
|
max_depth: Maximum depth to show (None for unlimited)
|
|
57
58
|
show_values: Whether to show before/after values
|
|
58
59
|
"""
|
|
59
|
-
|
|
60
|
+
|
|
60
61
|
if not isinstance(diff, Diff):
|
|
61
62
|
console.print("[red]Invalid diff object[/red]")
|
|
62
63
|
return
|
|
63
|
-
|
|
64
|
+
|
|
64
65
|
if diff.is_empty():
|
|
65
66
|
console.print("[green]✓ No changes detected[/green]")
|
|
66
67
|
return
|
|
67
|
-
|
|
68
|
+
|
|
68
69
|
# Create colored title with summary
|
|
69
70
|
summary = diff.summary()
|
|
70
71
|
title = Text("🔀 Configuration Changes ", style="bold")
|
|
71
72
|
title.append("(", style="dim")
|
|
72
|
-
|
|
73
|
+
|
|
73
74
|
parts = []
|
|
74
75
|
if summary.get("added", 0) > 0:
|
|
75
76
|
parts.append((f"+{summary['added']}", "green"))
|
|
@@ -77,20 +78,20 @@ def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
|
77
78
|
parts.append((f"-{summary['removed']}", "red"))
|
|
78
79
|
if summary.get("changed", 0) > 0:
|
|
79
80
|
parts.append((f"~{summary['changed']}", "yellow"))
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
for i, (text, color) in enumerate(parts):
|
|
82
83
|
if i > 0:
|
|
83
84
|
title.append(" ", style="dim")
|
|
84
85
|
title.append(text, style=f"bold {color}")
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
title.append(")", style="dim")
|
|
87
|
-
|
|
88
|
+
|
|
88
89
|
tree = Tree(title)
|
|
89
|
-
|
|
90
|
+
|
|
90
91
|
# Build a hierarchical tree structure based on component paths
|
|
91
92
|
# We'll use a dict to track nodes: path_tuple -> tree_node
|
|
92
93
|
path_nodes = {}
|
|
93
|
-
|
|
94
|
+
|
|
94
95
|
# Group changes by top-level section first to get counts
|
|
95
96
|
sections = {}
|
|
96
97
|
for change in diff.get_all_changes():
|
|
@@ -98,28 +99,28 @@ def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
|
98
99
|
if section not in sections:
|
|
99
100
|
sections[section] = []
|
|
100
101
|
sections[section].append(change)
|
|
101
|
-
|
|
102
|
+
|
|
102
103
|
# Process each change and build nested structure
|
|
103
104
|
for section_name in sorted(sections.keys()):
|
|
104
105
|
section_changes = sections[section_name]
|
|
105
|
-
|
|
106
|
+
|
|
106
107
|
# Create section node
|
|
107
108
|
section_label = Text()
|
|
108
109
|
section_label.append(section_name, style="bold cyan")
|
|
109
110
|
section_label.append(f" ({len(section_changes)} changes)", style="dim")
|
|
110
111
|
section_node = tree.add(section_label)
|
|
111
112
|
path_nodes[(section_name,)] = section_node
|
|
112
|
-
|
|
113
|
+
|
|
113
114
|
# Add each change, building nested path structure
|
|
114
115
|
for change in section_changes:
|
|
115
116
|
# Build path incrementally
|
|
116
117
|
path = tuple(change.path)
|
|
117
|
-
|
|
118
|
+
|
|
118
119
|
# Find or create parent nodes for nested path
|
|
119
120
|
parent_node = section_node
|
|
120
121
|
for i in range(1, len(path)):
|
|
121
|
-
partial_path = path[:i+1]
|
|
122
|
-
|
|
122
|
+
partial_path = path[: i + 1]
|
|
123
|
+
|
|
123
124
|
# Check if this is the final component or an intermediate path
|
|
124
125
|
if i == len(path) - 1:
|
|
125
126
|
# This is the actual component with the change
|
|
@@ -132,35 +133,37 @@ def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
|
132
133
|
else: # CHANGE
|
|
133
134
|
style = "yellow"
|
|
134
135
|
prefix = "~ "
|
|
135
|
-
|
|
136
|
+
|
|
136
137
|
# Component label
|
|
137
138
|
comp_label = Text()
|
|
138
139
|
comp_label.append(prefix, style=f"bold {style}")
|
|
139
140
|
comp_label.append(f"{change.component_type}: ", style=f"dim {style}")
|
|
140
141
|
comp_label.append(change.component_name, style=style)
|
|
141
|
-
|
|
142
|
+
|
|
142
143
|
comp_node = parent_node.add(comp_label)
|
|
143
|
-
|
|
144
|
+
|
|
144
145
|
if show_values:
|
|
145
146
|
# Show attributes for ADD operations
|
|
146
147
|
if change.op == ComponentDiffOp.ADD and change.after_dict:
|
|
147
148
|
for attr_name, attr_value in change.after_dict.items():
|
|
148
149
|
# Skip metadata and type fields
|
|
149
|
-
if attr_name in [
|
|
150
|
+
if attr_name in ["metadata", "type"]:
|
|
150
151
|
continue
|
|
151
152
|
attr_label = Text()
|
|
152
153
|
attr_label.append(" ", style="dim")
|
|
153
154
|
attr_label.append(attr_name, style="cyan")
|
|
154
155
|
attr_label.append(": ", style="dim")
|
|
155
156
|
formatted_val = _format_value(attr_value)
|
|
156
|
-
attr_label.append(
|
|
157
|
+
attr_label.append(
|
|
158
|
+
formatted_val, style="dim green" if formatted_val == "null" else "green"
|
|
159
|
+
)
|
|
157
160
|
comp_node.add(attr_label)
|
|
158
|
-
|
|
161
|
+
|
|
159
162
|
# Show attributes for REMOVE operations
|
|
160
163
|
elif change.op == ComponentDiffOp.REMOVE and change.before_dict:
|
|
161
164
|
for attr_name, attr_value in change.before_dict.items():
|
|
162
165
|
# Skip metadata and type fields
|
|
163
|
-
if attr_name in [
|
|
166
|
+
if attr_name in ["metadata", "type"]:
|
|
164
167
|
continue
|
|
165
168
|
attr_label = Text()
|
|
166
169
|
attr_label.append(" ", style="dim")
|
|
@@ -169,7 +172,7 @@ def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
|
169
172
|
formatted_val = _format_value(attr_value)
|
|
170
173
|
attr_label.append(formatted_val, style="dim red" if formatted_val == "null" else "red")
|
|
171
174
|
comp_node.add(attr_label)
|
|
172
|
-
|
|
175
|
+
|
|
173
176
|
# Show changed attributes for CHANGE operations
|
|
174
177
|
elif change.op == ComponentDiffOp.CHANGE and change.changed_attributes:
|
|
175
178
|
for attr in change.changed_attributes:
|
|
@@ -191,43 +194,41 @@ def print_diff_tree(diff, max_depth: int = None, show_values: bool = True):
|
|
|
191
194
|
path_node = parent_node.add(path_label)
|
|
192
195
|
path_nodes[partial_path] = path_node
|
|
193
196
|
parent_node = path_nodes[partial_path]
|
|
194
|
-
|
|
197
|
+
|
|
195
198
|
console.print(tree)
|
|
196
199
|
|
|
197
200
|
|
|
198
201
|
def print_diff_compact(diff, show_unchanged: bool = False):
|
|
199
202
|
"""
|
|
200
203
|
Print diff in a compact, git-like format.
|
|
201
|
-
|
|
204
|
+
|
|
202
205
|
Args:
|
|
203
206
|
diff: Diff object from configdiffer
|
|
204
207
|
show_unchanged: Whether to show unchanged paths
|
|
205
208
|
"""
|
|
206
|
-
|
|
209
|
+
|
|
207
210
|
if not isinstance(diff, Diff):
|
|
208
211
|
console.print("[red]Invalid diff object[/red]")
|
|
209
212
|
return
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
if diff.is_empty():
|
|
212
215
|
console.print("[green]✓ No changes detected[/green]")
|
|
213
216
|
return
|
|
214
|
-
|
|
217
|
+
|
|
215
218
|
if diff.added:
|
|
216
219
|
console.print("\n[bold green]✚ Added:[/bold green]")
|
|
217
220
|
for change in diff.added:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
+
console.print(f" [green]+ {change.component_type_name}:[/green] {change.component_name}")
|
|
222
|
+
|
|
221
223
|
if diff.removed:
|
|
222
224
|
console.print("\n[bold red]✖ Removed:[/bold red]")
|
|
223
225
|
for change in diff.removed:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
console.print(f" [red]- {change.component_type_name}:[/red] {change.component_name}")
|
|
227
|
+
|
|
227
228
|
if diff.changed:
|
|
228
229
|
console.print("\n[bold yellow]⚡ Modified:[/bold yellow]")
|
|
229
230
|
for change in diff.changed:
|
|
230
|
-
console.print(f" [yellow]~ {change.
|
|
231
|
+
console.print(f" [yellow]~ {change.component_type_name}:[/yellow] {change.component_name}")
|
|
231
232
|
if change.changed_attributes:
|
|
232
233
|
for attr in change.changed_attributes:
|
|
233
234
|
before_str = _format_value(attr.before, max_length=40)
|
|
@@ -240,19 +241,19 @@ def print_diff_compact(diff, show_unchanged: bool = False):
|
|
|
240
241
|
def print_diff_flat(diff):
|
|
241
242
|
"""
|
|
242
243
|
Print diff in a flat list format, good for quick scanning.
|
|
243
|
-
|
|
244
|
+
|
|
244
245
|
Args:
|
|
245
246
|
diff: Diff object from configdiffer
|
|
246
247
|
"""
|
|
247
|
-
|
|
248
|
+
|
|
248
249
|
if not isinstance(diff, Diff):
|
|
249
250
|
console.print("[red]Invalid diff object[/red]")
|
|
250
251
|
return
|
|
251
|
-
|
|
252
|
+
|
|
252
253
|
if diff.is_empty():
|
|
253
254
|
console.print("[green]✓ No changes detected[/green]")
|
|
254
255
|
return
|
|
255
|
-
|
|
256
|
+
|
|
256
257
|
# Create table
|
|
257
258
|
table = Table(title="Configuration Changes", show_lines=True)
|
|
258
259
|
table.add_column("Op", style="bold", width=3)
|
|
@@ -261,69 +262,69 @@ def print_diff_flat(diff):
|
|
|
261
262
|
table.add_column("Attribute", style="magenta")
|
|
262
263
|
table.add_column("Before", style="red", max_width=30)
|
|
263
264
|
table.add_column("After", style="green", max_width=30)
|
|
264
|
-
|
|
265
|
+
|
|
265
266
|
for change in diff.added:
|
|
266
267
|
table.add_row(
|
|
267
268
|
"[green]+[/green]",
|
|
268
|
-
change.
|
|
269
|
+
change.component_type_name,
|
|
269
270
|
change.component_name,
|
|
270
271
|
"[dim]--[/dim]",
|
|
271
272
|
"",
|
|
272
|
-
"[dim]new component[/dim]"
|
|
273
|
+
"[dim]new component[/dim]",
|
|
273
274
|
)
|
|
274
|
-
|
|
275
|
+
|
|
275
276
|
for change in diff.removed:
|
|
276
277
|
table.add_row(
|
|
277
278
|
"[red]-[/red]",
|
|
278
|
-
change.
|
|
279
|
+
change.component_type_name,
|
|
279
280
|
change.component_name,
|
|
280
281
|
"[dim]--[/dim]",
|
|
281
282
|
"[dim]component deleted[/dim]",
|
|
282
|
-
""
|
|
283
|
+
"",
|
|
283
284
|
)
|
|
284
|
-
|
|
285
|
+
|
|
285
286
|
for change in diff.changed:
|
|
286
287
|
if change.changed_attributes:
|
|
287
288
|
for i, attr in enumerate(change.changed_attributes):
|
|
288
289
|
table.add_row(
|
|
289
290
|
"[yellow]~[/yellow]" if i == 0 else "",
|
|
290
|
-
change.
|
|
291
|
+
change.component_type_name if i == 0 else "",
|
|
291
292
|
change.component_name if i == 0 else "",
|
|
292
293
|
attr.attribute_name,
|
|
293
294
|
_format_value(attr.before, max_length=30),
|
|
294
|
-
_format_value(attr.after, max_length=30)
|
|
295
|
+
_format_value(attr.after, max_length=30),
|
|
295
296
|
)
|
|
296
297
|
else:
|
|
297
298
|
table.add_row(
|
|
298
299
|
"[yellow]~[/yellow]",
|
|
299
|
-
change.
|
|
300
|
+
change.component_type_name,
|
|
300
301
|
change.component_name,
|
|
301
302
|
"[dim]--[/dim]",
|
|
302
303
|
"[dim]changed[/dim]",
|
|
303
|
-
"[dim]changed[/dim]"
|
|
304
|
+
"[dim]changed[/dim]",
|
|
304
305
|
)
|
|
305
|
-
|
|
306
|
+
|
|
306
307
|
console.print(table)
|
|
307
308
|
|
|
308
309
|
|
|
309
310
|
def _format_value(value, max_length: int = 100) -> str:
|
|
310
311
|
"""Format a value for display, truncating if necessary."""
|
|
311
|
-
|
|
312
|
+
|
|
312
313
|
if value is None:
|
|
313
314
|
return "null"
|
|
314
|
-
|
|
315
|
+
|
|
315
316
|
# Check if this is an AttributeValue (Pydantic model or dict with 'value' key)
|
|
316
|
-
if isinstance(value, BaseModel) and hasattr(value,
|
|
317
|
+
if isinstance(value, BaseModel) and hasattr(value, "value"):
|
|
317
318
|
value = value.value
|
|
318
319
|
if value is None:
|
|
319
320
|
return "null"
|
|
320
|
-
elif isinstance(value, dict) and
|
|
321
|
-
value = value[
|
|
321
|
+
elif isinstance(value, dict) and "value" in value:
|
|
322
|
+
value = value["value"]
|
|
322
323
|
if value is None:
|
|
323
324
|
return "null"
|
|
324
|
-
|
|
325
|
+
|
|
325
326
|
if isinstance(value, BaseModel):
|
|
326
|
-
value_str = json.dumps(value.model_dump(), separators=(
|
|
327
|
+
value_str = json.dumps(value.model_dump(), separators=(",", ":"))
|
|
327
328
|
elif isinstance(value, (dict, list)):
|
|
328
329
|
# Convert any nested BaseModel instances before serializing
|
|
329
330
|
def _to_serializable(obj):
|
|
@@ -334,12 +335,13 @@ def _format_value(value, max_length: int = 100) -> str:
|
|
|
334
335
|
if isinstance(obj, list):
|
|
335
336
|
return [_to_serializable(i) for i in obj]
|
|
336
337
|
return obj
|
|
337
|
-
|
|
338
|
+
|
|
339
|
+
value_str = json.dumps(_to_serializable(value), separators=(",", ":"))
|
|
338
340
|
else:
|
|
339
341
|
value_str = str(value)
|
|
340
|
-
|
|
342
|
+
|
|
341
343
|
# Truncate if too long
|
|
342
344
|
if len(value_str) > max_length:
|
|
343
|
-
return value_str[:max_length-3] + "..."
|
|
344
|
-
|
|
345
|
+
return value_str[: max_length - 3] + "..."
|
|
346
|
+
|
|
345
347
|
return value_str
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"""Main CLI application entry point."""
|
|
2
2
|
|
|
3
|
-
import typer
|
|
4
3
|
import importlib
|
|
5
|
-
import pkgutil
|
|
6
4
|
import pathlib
|
|
5
|
+
import pkgutil
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
7
9
|
from acex_cli.context import CLIContext
|
|
10
|
+
from acex_cli.output import console
|
|
8
11
|
|
|
9
12
|
app = typer.Typer(
|
|
10
|
-
name="acex",
|
|
11
|
-
help="ACE-X - Automation & Control Ecosystem CLI",
|
|
12
|
-
add_completion=True,
|
|
13
|
-
pretty_exceptions_enable=False
|
|
13
|
+
name="acex", help="ACE-X - Automation & Control Ecosystem CLI", add_completion=True, pretty_exceptions_enable=False
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
|
|
@@ -38,9 +38,9 @@ def main(ctx: typer.Context):
|
|
|
38
38
|
def version():
|
|
39
39
|
"""Show version information."""
|
|
40
40
|
from acex_cli import __version__
|
|
41
|
+
|
|
41
42
|
console.print(f"ACE-X CLI version: {__version__}")
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
if __name__ == "__main__":
|
|
45
46
|
app()
|
|
46
|
-
|
|
@@ -7,10 +7,9 @@ header control, and pagination display.
|
|
|
7
7
|
import csv
|
|
8
8
|
import io
|
|
9
9
|
import json
|
|
10
|
-
from typing import Optional
|
|
11
10
|
|
|
12
|
-
from rich.table import Table
|
|
13
11
|
from rich.console import Console
|
|
12
|
+
from rich.table import Table
|
|
14
13
|
|
|
15
14
|
console = Console()
|
|
16
15
|
|
|
@@ -19,7 +18,7 @@ def display_list(
|
|
|
19
18
|
result,
|
|
20
19
|
*,
|
|
21
20
|
format: str = "table",
|
|
22
|
-
columns:
|
|
21
|
+
columns: list[str] | None = None,
|
|
23
22
|
no_header: bool = False,
|
|
24
23
|
model=None,
|
|
25
24
|
title: str = "",
|
|
@@ -47,7 +46,10 @@ def display_list(
|
|
|
47
46
|
_print_csv(rows, columns, no_header)
|
|
48
47
|
else:
|
|
49
48
|
_print_table(
|
|
50
|
-
rows,
|
|
49
|
+
rows,
|
|
50
|
+
columns,
|
|
51
|
+
no_header,
|
|
52
|
+
title,
|
|
51
53
|
total=total,
|
|
52
54
|
limit=getattr(result, "limit", None),
|
|
53
55
|
offset=getattr(result, "offset", None),
|
|
@@ -58,8 +60,8 @@ def display_object(
|
|
|
58
60
|
obj,
|
|
59
61
|
*,
|
|
60
62
|
format: str = "table",
|
|
61
|
-
columns:
|
|
62
|
-
exclude:
|
|
63
|
+
columns: list[str] | None = None,
|
|
64
|
+
exclude: list[str] | None = None,
|
|
63
65
|
model=None,
|
|
64
66
|
title: str = "",
|
|
65
67
|
):
|
|
@@ -221,6 +223,7 @@ def _format_cell(value) -> str:
|
|
|
221
223
|
if value is None:
|
|
222
224
|
return ""
|
|
223
225
|
from enum import Enum
|
|
226
|
+
|
|
224
227
|
if isinstance(value, Enum):
|
|
225
228
|
return value.value
|
|
226
229
|
if isinstance(value, dict) and not value:
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
from acex_client.acex.acex import Acex
|
|
2
|
-
import requests
|
|
3
|
-
from requests.exceptions import ConnectionError, SSLError
|
|
4
2
|
|
|
5
3
|
|
|
6
4
|
def _innermost_exception_message(exc: Exception) -> str:
|
|
@@ -16,7 +14,6 @@ def get_sdk(context):
|
|
|
16
14
|
url = context.get("url")
|
|
17
15
|
if not url:
|
|
18
16
|
raise RuntimeError("Active context saknar 'url'.")
|
|
19
|
-
jwt = context.get("jwt")
|
|
20
17
|
|
|
21
18
|
verify = context.get("verify_ssl", True)
|
|
22
19
|
client = Acex(baseurl=f"{url}/", verify=verify)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|