acex-cli 4.5.0__tar.gz → 4.6.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: acex-cli
3
- Version: 4.5.0
3
+ Version: 4.6.0
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,13 +11,14 @@ 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.3.0,<5.0.0)
15
- Requires-Dist: acex-devkit (>=1.6.2,<2.0.0)
16
- Requires-Dist: acex-driver-cisco-ioscli (>=0.5.1,<0.6.0)
17
- Requires-Dist: click (==8.1.7)
18
- Requires-Dist: ntc-templates (>=8.1.0,<9.0.0)
19
- Requires-Dist: rich (>=13.0.0,<14.0.0)
20
- Requires-Dist: typer (>=0.12.0,<0.13.0)
14
+ Classifier: Programming Language :: Python :: 3.15
15
+ Requires-Dist: acex-client (>=4.11.0,<5.0.0)
16
+ Requires-Dist: acex-devkit (>=2.11.0,<3.0.0)
17
+ Requires-Dist: acex-driver-cisco-ioscli (>=2.7.4,<3.0.0)
18
+ Requires-Dist: click (>=8.1.7)
19
+ Requires-Dist: ntc-templates (>=8.1.0)
20
+ Requires-Dist: rich (>=13.0.0)
21
+ Requires-Dist: typer (>=0.12.0)
21
22
  Project-URL: Homepage, https://github.com/acex-labs/acex
22
23
  Project-URL: Repository, https://github.com/acex-labs/acex
23
24
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "acex-cli"
3
- version = "4.5.0"
3
+ version = "4.6.0"
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"
@@ -14,17 +14,21 @@ packages = [
14
14
 
15
15
  [tool.poetry.dependencies]
16
16
  python = "^3.13"
17
- typer = "^0.12.0"
18
- rich = "^13.0.0"
19
- click = "8.1.7"
20
- acex-client = "^4.3.0" # Changes to real dep. by switch_acex_client_dep for publishing
21
- acex-devkit = "^1.6.2"
22
- acex-driver-cisco-ioscli = "^0.5.1"
23
- ntc-templates = "^8.1.0"
17
+ typer = ">=0.12.0"
18
+ rich = ">=13.0.0"
19
+ click = ">=8.1.7"
20
+ acex-client = "^4.11.0" # Changes to real dep. by switch_acex_client_dep for publishing
21
+ acex-devkit = "^2.11.0"
22
+ acex-driver-cisco-ioscli = "^2.7.4"
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"
@@ -0,0 +1,5 @@
1
+ """ACE-X CLI - Command-line interface for ACE-X."""
2
+
3
+ import importlib.metadata
4
+
5
+ __version__ = importlib.metadata.version("acex-cli")
@@ -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 acex_client.models.generated_models import AssetResponse
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: Optional[str] = typer.Option(None, help="Filter by vendor (prefix match)"),
17
- os: Optional[str] = typer.Option(None, "--os", help="Filter by OS"),
18
- hardware_model: Optional[str] = typer.Option(None, help="Filter by hardware model"),
19
- ned_id: Optional[str] = typer.Option(None, help="Filter by NED ID"),
20
- serial_number: Optional[str] = typer.Option(None, help="Filter by serial number"),
21
- assigned: Optional[bool] = typer.Option(None, help="Filter assigned/unassigned"),
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: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated 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, os=os, hardware_model=hardware_model,
34
- ned_id=ned_id, serial_number=serial_number, assigned=assigned,
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: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
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 acex_client.models.generated_models import LogicalNode
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: Optional[str] = typer.Option(None, help="Filter by role (prefix match)"),
17
- site: Optional[str] = typer.Option(None, help="Filter by site (prefix match)"),
18
- hostname: Optional[str] = typer.Option(None, help="Filter by hostname (prefix match)"),
19
- sequence: Optional[int] = typer.Option(None, help="Filter by sequence number"),
20
- assigned: Optional[bool] = typer.Option(None, help="Filter assigned/unassigned"),
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: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated 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, site=site, hostname=hostname,
33
- sequence=sequence, assigned=assigned,
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: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
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 acex_client.models.generated_models import Ned
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: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated 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: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
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
- def _request_get(sdk, path: str, params: Optional[dict] = None) -> requests.Response:
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/?node_id={node_id}").json()
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,7 +56,8 @@ 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
60
- display_list(result, columns=["id", "hostname", "site", "status"], title="")
59
+
60
+ display_list(result, columns=["id", "hostname", "site", "admin_status", "provision_status"], title="")
61
61
  raise typer.Exit(1)
62
62
  node = sdk.node_instances.get(result.items[0].id)
63
63
  if not node:
@@ -83,24 +83,25 @@ 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
- display_list(result, columns=["id", "hostname", "site", "status"], title="")
86
+
87
+ display_list(result, columns=["id", "hostname", "site", "admin_status", "provision_status"], title="")
87
88
  raise typer.Exit(1)
88
89
  return result.items[0]
89
90
 
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
- except ImportError:
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,34 +196,45 @@ 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: Optional[str] = typer.Option(None, help="Filter by site (prefix match)"),
203
- hostname: Optional[str] = typer.Option(None, help="Filter by hostname (prefix match)"),
204
- logical_node_id: Optional[int] = typer.Option(None, help="Filter by logical node ID"),
205
- asset_ref_id: Optional[int] = typer.Option(None, help="Filter by asset ref ID"),
206
- status: Optional[str] = typer.Option(None, help="Filter by status (planned, init, active, decommissioned)"),
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
+ admin_status: str | None = typer.Option(None, help="Filter by admin status (planned, active, decommissioned)"),
209
+ provision_status: str | None = typer.Option(
210
+ None,
211
+ help=("Filter by provision status (unprovisioned, adopted, enrolled, provisioning, provisioned, failed)"),
212
+ ),
207
213
  # Pagination
208
214
  limit: int = typer.Option(100, "--limit", "-l", help="Max items to return"),
209
215
  offset: int = typer.Option(0, "--offset", help="Items to skip"),
210
216
  # Output
211
217
  format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
212
- columns: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated columns"),
218
+ columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated columns"),
213
219
  no_header: bool = typer.Option(False, "--no-header", help="Hide table header"),
214
220
  ):
215
221
  """List node instances with optional filters."""
216
222
  sdk = get_sdk(ctx.obj.get_active_context())
217
223
  filters = _compact(
218
- site=site, hostname=hostname, logical_node_id=logical_node_id,
219
- asset_ref_id=asset_ref_id, status=status,
224
+ site=site,
225
+ hostname=hostname,
226
+ logical_node_id=logical_node_id,
227
+ asset_ref_id=asset_ref_id,
228
+ admin_status=admin_status,
229
+ provision_status=provision_status,
220
230
  )
221
231
  result = sdk.node_instances.query(limit=limit, offset=offset, **filters)
222
232
  display_list(
223
233
  result,
224
234
  format=format,
225
- columns=columns.split(",") if columns else ["id", "hostname", "site", "status", "vendor", "os", "ned_id"],
235
+ columns=columns.split(",")
236
+ if columns
237
+ else ["id", "hostname", "site", "admin_status", "provision_status", "vendor", "os", "ned_id"],
226
238
  no_header=no_header,
227
239
  model=NodeListItem,
228
240
  title="Node Instances",
@@ -234,7 +246,7 @@ def show_cmd(
234
246
  ctx: typer.Context,
235
247
  node: str = typer.Argument(help="Node ID or hostname"),
236
248
  format: str = typer.Option("table", "--format", "-f", help="Output format: table, json, csv"),
237
- columns: Optional[str] = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
249
+ columns: str | None = typer.Option(None, "--columns", "-c", help="Comma-separated fields"),
238
250
  ):
239
251
  """Show details for a node instance (by ID or hostname)."""
240
252
  sdk = get_sdk(ctx.obj.get_active_context())
@@ -277,7 +289,7 @@ def connect_cmd(
277
289
  connections = _get_connection(sdk, node_id)
278
290
  except Exception as e:
279
291
  typer.echo(f"Failed to fetch management connection: {e}")
280
- raise typer.Exit(1)
292
+ raise typer.Exit(1) from e
281
293
 
282
294
  if not connections:
283
295
  typer.echo(f"No management connection found for node '{node}'.")
@@ -304,13 +316,14 @@ def connect_cmd(
304
316
 
305
317
  # ── Config show commands ────────────────────────────────────────
306
318
 
319
+
307
320
  @config_show_app.command("local")
308
321
  def config_show_local(
309
322
  ctx: typer.Context,
310
323
  node: str = typer.Argument(help="Node ID or hostname"),
311
324
  dir: str = typer.Option(..., "--dir", "-d", help="Path to local config_maps directory"),
312
325
  render: bool = typer.Option(False, "--render", help="Show rendered device config"),
313
- path: Optional[str] = typer.Option(None, "--path", "-p", help="Drill into section"),
326
+ path: str | None = typer.Option(None, "--path", "-p", help="Drill into section"),
314
327
  format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
315
328
  ):
316
329
  """Compile config locally from config maps on disk."""
@@ -330,7 +343,7 @@ def config_show_desired(
330
343
  ctx: typer.Context,
331
344
  node: str = typer.Argument(help="Node ID or hostname"),
332
345
  render: bool = typer.Option(False, "--render", help="Show rendered device config"),
333
- path: Optional[str] = typer.Option(None, "--path", "-p", help="Drill into section, e.g. system.ntp, interfaces"),
346
+ path: str | None = typer.Option(None, "--path", "-p", help="Drill into section, e.g. system.ntp, interfaces"),
334
347
  format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
335
348
  ):
336
349
  """Show desired config."""
@@ -352,7 +365,7 @@ def config_show_observed(
352
365
  ctx: typer.Context,
353
366
  node: str = typer.Argument(help="Node ID or hostname"),
354
367
  render: bool = typer.Option(False, "--render", help="Show rendered device config"),
355
- path: Optional[str] = typer.Option(None, "--path", "-p", help="Drill into section, e.g. system.ntp, interfaces"),
368
+ path: str | None = typer.Option(None, "--path", "-p", help="Drill into section, e.g. system.ntp, interfaces"),
356
369
  format: str = typer.Option("table", "--format", "-f", help="Output format: table, json"),
357
370
  ):
358
371
  """Show observed (backup) config."""
@@ -378,21 +391,24 @@ def config_show_observed(
378
391
 
379
392
  # ── Config diff commands ────────────────────────────────────────
380
393
 
394
+
381
395
  @config_diff_app.command("plan")
382
396
  def config_diff_plan(
383
397
  ctx: typer.Context,
384
398
  node: str = typer.Argument(help="Node ID or hostname"),
385
399
  format: str = typer.Option(
386
- "tree", "--format", "-f",
400
+ "tree",
401
+ "--format",
402
+ "-f",
387
403
  help="Output format: tree, compact, flat, summary, json, commands",
388
404
  ),
389
- max_depth: Optional[int] = typer.Option(None, "--max-depth", "-d", help="Max depth for tree view"),
405
+ max_depth: int | None = typer.Option(None, "--max-depth", "-d", help="Max depth for tree view"),
390
406
  no_values: bool = typer.Option(False, "--no-values", help="Hide values in tree view"),
391
407
  ):
392
408
  """Show planned diff between observed and desired config."""
393
409
  sdk = get_sdk(ctx.obj.get_active_context())
394
410
  node_instance, desired_config = _fetch_desired_config(sdk, node)
395
- node_id = str(node_instance.id) if hasattr(node_instance, 'id') and node_instance.id else node
411
+ node_id = str(node_instance.id) if hasattr(node_instance, "id") and node_instance.id else node
396
412
 
397
413
  observed_raw = _get_latest_observed_config(sdk, node_id, "parsed").get("content")
398
414
  observed_config = ComposedConfiguration(**observed_raw)
@@ -425,7 +441,7 @@ def config_diff_apply(
425
441
  """Compute diff and apply config patch to device."""
426
442
  sdk = get_sdk(ctx.obj.get_active_context())
427
443
  node_instance, desired_config = _fetch_desired_config(sdk, node)
428
- node_id = str(node_instance.id) if hasattr(node_instance, 'id') and node_instance.id else node
444
+ node_id = str(node_instance.id) if hasattr(node_instance, "id") and node_instance.id else node
429
445
 
430
446
  observed_raw = _get_latest_observed_config(sdk, node_id, "parsed").get("content")
431
447
  observed_config = ComposedConfiguration(**observed_raw)
@@ -445,11 +461,13 @@ def config_diff_apply(
445
461
 
446
462
  # ── Config display helpers ──────────────────────────────────────
447
463
 
464
+
448
465
  def _print_rendered(text: str):
449
466
  """Print rendered device config with syntax highlighting."""
450
467
  from rich.console import Console
451
- from rich.syntax import Syntax
452
468
  from rich.panel import Panel
469
+ from rich.syntax import Syntax
470
+
453
471
  console = Console()
454
472
  syntax = Syntax(text, "cisco", theme="monokai", line_numbers=True, word_wrap=True)
455
473
  console.print(Panel(syntax, border_style="dim"))
@@ -506,10 +524,11 @@ def _count_components(data) -> int:
506
524
  return count
507
525
 
508
526
 
509
- def _print_parsed_config(data: dict, path: Optional[str], format: str, title: str):
527
+ def _print_parsed_config(data: dict, path: str | None, format: str, title: str):
510
528
  """Display parsed config with AttributeValue awareness."""
511
529
  from rich.console import Console
512
530
  from rich.table import Table
531
+
513
532
  console = Console()
514
533
 
515
534
  if format == "json":
@@ -527,7 +546,11 @@ def _print_parsed_config(data: dict, path: Optional[str], format: str, title: st
527
546
  console.print(f" [dim]source: {source}[/dim]")
528
547
  return
529
548
  # Dict of AttributeValues (e.g. system.config)
530
- if isinstance(section, dict) and section and all(_is_attribute_value(v) for v in section.values() if isinstance(v, dict) and v):
549
+ if (
550
+ isinstance(section, dict)
551
+ and section
552
+ and all(_is_attribute_value(v) for v in section.values() if isinstance(v, dict) and v)
553
+ ):
531
554
  _print_attributes_table(console, section, title=path)
532
555
  return
533
556
  # Container with named entries (e.g. interfaces, ntp.servers)
@@ -553,12 +576,15 @@ def _print_parsed_config(data: dict, path: Optional[str], format: str, title: st
553
576
  )
554
577
 
555
578
  console.print(table)
556
- console.print("[dim]Use --path <section> to drill in, --format json for full dump, --render for device config[/dim]")
579
+ console.print(
580
+ "[dim]Use --path <section> to drill in, --format json for full dump, --render for device config[/dim]"
581
+ )
557
582
 
558
583
 
559
584
  def _print_attributes_table(console, data: dict, title: str = ""):
560
585
  """Print a dict where values are AttributeValues as a clean table."""
561
586
  from rich.table import Table
587
+
562
588
  table = Table(title=title, show_header=True, title_style="bold", title_justify="left")
563
589
  table.add_column("Attribute", style="cyan")
564
590
  table.add_column("Value", style="white")
@@ -578,7 +604,6 @@ def _print_attributes_table(console, data: dict, title: str = ""):
578
604
 
579
605
  def _print_config_section(console, data: dict, path: str):
580
606
  """Print a config section, handling containers and nested AttributeValues."""
581
- from rich.table import Table
582
607
 
583
608
  # Check if entries are named components (e.g. interfaces, ntp.servers)
584
609
  for entry_name, entry_data in data.items():
@@ -611,6 +636,7 @@ def _print_config_section(console, data: dict, path: str):
611
636
 
612
637
  # ── Util ────────────────────────────────────────────────────────
613
638
 
639
+
614
640
  def _flatten_node(node, node_id: str = None) -> dict:
615
641
  """Flatten NodeResponse into a single-level dict for display."""
616
642
  data = node.model_dump() if hasattr(node, "model_dump") else dict(node)
@@ -622,7 +648,8 @@ def _flatten_node(node, node_id: str = None) -> dict:
622
648
  "hostname": logical_node.get("hostname"),
623
649
  "site": logical_node.get("site"),
624
650
  "role": logical_node.get("role"),
625
- "status": data.get("status"),
651
+ "admin_status": data.get("admin_status"),
652
+ "provision_status": data.get("provision_status"),
626
653
  "vendor": asset.get("vendor"),
627
654
  "os": asset.get("os"),
628
655
  "os_version": asset.get("os_version"),
@@ -653,7 +680,8 @@ def _resolve_node(sdk, identifier: str):
653
680
  elif len(result) > 1:
654
681
  typer.echo(f"Multiple nodes match hostname '{identifier}':")
655
682
  from acex_cli.output import display_list
656
- display_list(result, columns=["id", "hostname", "site", "status"], title="")
683
+
684
+ display_list(result, columns=["id", "hostname", "site", "admin_status", "provision_status"], title="")
657
685
  typer.echo("Use the ID to specify which node.")
658
686
  raise typer.Exit(1)
659
687
 
@@ -672,7 +700,8 @@ def _resolve_node_id(sdk, identifier: str) -> str:
672
700
  elif len(result) > 1:
673
701
  typer.echo(f"Multiple nodes match hostname '{identifier}':")
674
702
  from acex_cli.output import display_list
675
- display_list(result, columns=["id", "hostname", "site", "status"], title="")
703
+
704
+ display_list(result, columns=["id", "hostname", "site", "admin_status", "provision_status"], title="")
676
705
  typer.echo("Use the ID to specify which node.")
677
706
  raise typer.Exit(1)
678
707
  typer.echo(f"Node '{identifier}' not found.")
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
 
4
4
  CONTEXT_FILE = Path.home() / ".acex/.acex_cli_context.json"
5
5
 
6
+
6
7
  class CLIContext:
7
8
  def __init__(self):
8
9
  self.data = self._load()
@@ -1,15 +1,16 @@
1
1
  """Formatters for displaying configuration diffs in CLI."""
2
+
2
3
  import json
3
- from rich.tree import Tree
4
- from rich.text import Text
5
- from rich.table import Table
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 ['metadata', 'type']:
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(formatted_val, style="dim green" if formatted_val == "null" else "green")
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 ['metadata', 'type']:
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
- path = change.get_path_str()
219
- console.print(f" [green]+ {change.component_type_name_name}:[/green] {change.component_name}")
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
- path = change.get_path_str()
225
- console.print(f" [red]- {change.component_type_name_name}:[/red] {change.component_name}")
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.component_type_name_name}:[/yellow] {change.component_name}")
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.component_type_name_name,
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.component_type_name_name,
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.component_type_name_name if i == 0 else "",
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.component_type_name_name,
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, '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 'value' in value:
321
- value = 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
- value_str = json.dumps(_to_serializable(value), separators=(',', ':'))
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: Optional[list[str]] = None,
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, columns, no_header, title,
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: Optional[list[str]] = None,
62
- exclude: Optional[list[str]] = None,
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)
@@ -1,3 +0,0 @@
1
- """ACE-X CLI - Command-line interface for ACE-X."""
2
-
3
- __version__ = "0.2.0"
File without changes