acex-cli 4.6.0__tar.gz → 5.0.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.6.0
3
+ Version: 5.0.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
@@ -12,9 +12,9 @@ Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.13
13
13
  Classifier: Programming Language :: Python :: 3.14
14
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)
15
+ Requires-Dist: acex-client (>=4.12.0,<5.0.0)
16
+ Requires-Dist: acex-devkit (>=3.0.0,<4.0.0)
17
+ Requires-Dist: acex-driver-cisco-ioscli (>=5.1.0,<6.0.0)
18
18
  Requires-Dist: click (>=8.1.7)
19
19
  Requires-Dist: ntc-templates (>=8.1.0)
20
20
  Requires-Dist: rich (>=13.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "acex-cli"
3
- version = "4.6.0"
3
+ version = "5.0.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"
@@ -17,9 +17,9 @@ 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.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"
20
+ acex-client = "^4.12.0" # Changes to real dep. by switch_acex_client_dep for publishing
21
+ acex-devkit = "^3.0.0"
22
+ acex-driver-cisco-ioscli = "^5.1.0"
23
23
  ntc-templates = ">=8.1.0"
24
24
 
25
25
  [tool.poetry.scripts]
@@ -29,6 +29,13 @@ acex = "acex_cli.main:app"
29
29
  pytest = "^8.0.0"
30
30
  pytest-asyncio = "^0.23.0"
31
31
 
32
+ [tool.poetry.plugins."acex.changelog"]
33
+ acex-cli = "acex_cli"
34
+
35
+ # Shown as the component's name in Settings > What's new.
36
+ [tool.acex.changelog]
37
+ name = "ACEX CLI"
38
+
32
39
  [build-system]
33
40
  requires = ["poetry-core>=2.0.0,<3.0.0"]
34
41
  build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,498 @@
1
+ {
2
+ "schema_version": 1,
3
+ "component": "acex-cli",
4
+ "name": "ACEX CLI",
5
+ "releases": [
6
+ {
7
+ "version": "5.0.0",
8
+ "date": "2026-11-06",
9
+ "entries": [
10
+ {
11
+ "id": "20261013-node-status-is-split-into-admin-status-a",
12
+ "kind": "changed",
13
+ "title": "Node `status` is split into `admin_status` and `provision_status`",
14
+ "description": "`acex node list` no longer has `--status`; use `--admin-status` (planned, active, decommissioned) and `--provision-status` instead. The node table, `acex node show`, and JSON/CSV output now carry `admin_status` and `provision_status` instead of `status`, so scripts that read `status` or pass `--columns status` must be updated. Needs a server that has the same change.",
15
+ "breaking": true,
16
+ "audience": "user",
17
+ "refs": [
18
+ {
19
+ "label": "#292",
20
+ "url": null
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "version": "4.5.1",
28
+ "date": "2026-09-02",
29
+ "summary": "The compact and flat diff views work with changes, and `acex version` prints a version again.",
30
+ "entries": [
31
+ {
32
+ "id": "01-acex-node-config-diff-plan-f-compact-and",
33
+ "kind": "fixed",
34
+ "title": "`acex node config diff plan -f compact` and `-f flat` work with changes",
35
+ "description": "Both views used a field name that does not exist on the diff objects, so they should have stopped with a Python error whenever the diff was not empty. They now read the right field and list added, removed and changed components.",
36
+ "breaking": false,
37
+ "audience": "user",
38
+ "refs": [
39
+ {
40
+ "label": "#207",
41
+ "url": null
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "id": "02-acex-version-prints-a-version-again-but-",
47
+ "kind": "fixed",
48
+ "title": "`acex version` prints a version again, but it still says 0.2.0",
49
+ "description": "The command no longer fails on a missing output helper. The number it prints comes from a constant that was never updated, so it reports 0.2.0 whatever release is installed.",
50
+ "breaking": false,
51
+ "audience": "user",
52
+ "refs": [
53
+ {
54
+ "label": "#207",
55
+ "url": null
56
+ }
57
+ ]
58
+ },
59
+ {
60
+ "id": "03-management-connection-lookups-use-the-ap",
61
+ "kind": "fixed",
62
+ "title": "Management connection lookups use the API path without a trailing slash",
63
+ "description": "`acex node show` (management address, port and protocol), `acex node connect` and `acex node config diff apply` now request the node's management connection from the path without a trailing slash, matching the server change in this release. Against a server that expects the old path, these commands would fail to find the connection.",
64
+ "breaking": false,
65
+ "audience": "user",
66
+ "refs": [
67
+ {
68
+ "label": "#251",
69
+ "url": null
70
+ }
71
+ ]
72
+ }
73
+ ]
74
+ },
75
+ {
76
+ "version": "4.5.0",
77
+ "date": "2026-04-22",
78
+ "entries": [
79
+ {
80
+ "id": "01-acex-node-connect-node-opens-an-ssh-sess",
81
+ "kind": "added",
82
+ "title": "`acex node connect NODE` opens an SSH session to the node",
83
+ "description": "Looks up the node's management connection and starts `ssh` to its address, replacing the `acex` process. It uses your local `ssh` and login name, and exits with status 1 if there is no connection, it is not SSH, or it has no address.",
84
+ "breaking": false,
85
+ "audience": "user",
86
+ "refs": [
87
+ {
88
+ "label": "#126",
89
+ "url": null
90
+ }
91
+ ]
92
+ }
93
+ ]
94
+ },
95
+ {
96
+ "version": "4.4.1",
97
+ "date": "2026-04-22",
98
+ "entries": [
99
+ {
100
+ "id": "01-acex-node-show-also-shows-the-management",
101
+ "kind": "added",
102
+ "title": "`acex node show` also shows the management address, port and protocol",
103
+ "description": "The node details now include `mgmt_target_ip`, `mgmt_target_port` and `mgmt_protocol` from the node's first management connection. If the connection cannot be fetched, the command still prints the rest. The `id` field is filled in even when the API response leaves it out.",
104
+ "breaking": false,
105
+ "audience": "user",
106
+ "refs": [
107
+ {
108
+ "label": "#122",
109
+ "url": null
110
+ }
111
+ ]
112
+ }
113
+ ]
114
+ },
115
+ {
116
+ "version": "4.3.1",
117
+ "date": "2026-04-21",
118
+ "entries": [
119
+ {
120
+ "id": "01-acex-node-config-show-desired-no-longer-",
121
+ "kind": "fixed",
122
+ "title": "`acex node config show desired` no longer prints a debug timing line",
123
+ "description": "The \"[debug] sdk init: ...\" line that 4.3.0 printed after the output is gone, and `--render` no longer ends with a Python error.",
124
+ "breaking": false,
125
+ "audience": "user",
126
+ "refs": [
127
+ {
128
+ "label": "#119",
129
+ "url": null
130
+ }
131
+ ]
132
+ }
133
+ ]
134
+ },
135
+ {
136
+ "version": "4.3.0",
137
+ "date": "2026-04-21",
138
+ "entries": [
139
+ {
140
+ "id": "01-acex-logical-node-and-acex-ned-list-and-",
141
+ "kind": "added",
142
+ "title": "`acex logical-node` and `acex ned` list and show commands",
143
+ "description": "`acex logical-node list|show LOGICAL_NODE_ID` and `acex ned list|show NED_ID` print logical nodes and network element drivers. Command groups with underscores in their code name are typed with hyphens.",
144
+ "breaking": false,
145
+ "audience": "user",
146
+ "refs": [
147
+ {
148
+ "label": "#115",
149
+ "url": null
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ "id": "06-acex-node-config-show-local-node-dir-dir",
155
+ "kind": "added",
156
+ "title": "`acex node config show local NODE --dir DIR` compiles config from local files",
157
+ "description": "Builds the configuration for a node from the config map files in a local directory, without publishing them, and shows it like `show desired` (`--render`, `--path`, `--format` work the same). It needs the `acex` backend package installed and reports which config maps were found and matched.",
158
+ "breaking": false,
159
+ "audience": "user",
160
+ "refs": [
161
+ {
162
+ "label": "#115",
163
+ "url": null
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ "id": "03-filter-and-page-asset-list-node-list-and",
169
+ "kind": "added",
170
+ "title": "Filter and page `asset list`, `node list` and `logical-node list`",
171
+ "description": "`asset list` filters on `--vendor`, `--os`, `--hardware-model`, `--ned-id`, `--serial-number` and `--assigned`; `node list` on `--site`, `--hostname`, `--logical-node-id`, `--asset-ref-id` and `--status`; `logical-node list` on `--role`, `--site`, `--hostname`, `--sequence` and `--assigned`. All three return at most 100 rows unless `--limit` (`-l`) and `--offset` say otherwise, and the table shows \"Showing 1-100 of N\" when more exist.",
172
+ "breaking": false,
173
+ "audience": "user",
174
+ "refs": [
175
+ {
176
+ "label": "#115",
177
+ "url": null
178
+ }
179
+ ]
180
+ },
181
+ {
182
+ "id": "02-output-as-table-json-or-csv-with-format-",
183
+ "kind": "added",
184
+ "title": "Output as table, JSON or CSV with `--format`, `--columns` and `--no-header`",
185
+ "description": "All `list` and `show` commands for assets, nodes, logical nodes and NEDs take `--format` (`-f`) `table`, `json` or `csv`, plus `--columns` (`-c`) with a comma-separated list of columns. `list` commands also take `--no-header`, which makes the output easier to use in scripts.",
186
+ "breaking": false,
187
+ "audience": "user",
188
+ "refs": [
189
+ {
190
+ "label": "#115",
191
+ "url": null
192
+ }
193
+ ]
194
+ },
195
+ {
196
+ "id": "05-acex-node-config-show-desired-observed-g",
197
+ "kind": "changed",
198
+ "title": "`acex node config show desired|observed` give a section overview and `--path`",
199
+ "description": "Without `--render` they now print a table of configuration sections with component counts; `--path system.ntp` (or another section) drills in, and `--format json` dumps everything. `--render` prints the device configuration with line numbers and syntax highlighting.",
200
+ "breaking": false,
201
+ "audience": "user",
202
+ "refs": [
203
+ {
204
+ "label": "#115",
205
+ "url": null
206
+ }
207
+ ]
208
+ },
209
+ {
210
+ "id": "07-node-and-configuration-lookups-that-find",
211
+ "kind": "changed",
212
+ "title": "Node and configuration lookups that find nothing now exit with status 1",
213
+ "description": "`acex node show`, `config show observed`, `config diff plan` and `config diff apply` exit with status 1 for an unknown node or a missing configuration, with \"Node 'X' not found.\", \"No observed config found.\" or \"No desired configuration found.\". Earlier versions printed the message and exited with 0.",
214
+ "breaking": false,
215
+ "audience": "user",
216
+ "refs": [
217
+ {
218
+ "label": "#115",
219
+ "url": null
220
+ }
221
+ ]
222
+ },
223
+ {
224
+ "id": "04-node-commands-accept-a-hostname-as-well-",
225
+ "kind": "changed",
226
+ "title": "Node commands accept a hostname as well as an ID",
227
+ "description": "`acex node show`, `acex node config show ...` and `acex node config diff ...` take `NODE` as a numeric ID or a hostname. If several nodes share the hostname, they are listed and the command asks for the ID.",
228
+ "breaking": false,
229
+ "audience": "user",
230
+ "refs": [
231
+ {
232
+ "label": "#115",
233
+ "url": null
234
+ }
235
+ ]
236
+ },
237
+ {
238
+ "id": "09-the-could-not-connect-to-api-message-is-",
239
+ "kind": "changed",
240
+ "title": "The \"Could not connect to API\" message is no longer shown",
241
+ "description": "Commands no longer test the API address first, so an unreachable or untrusted server is no longer reported with that message. What appears instead comes from the client library, and its wording is not confirmed.",
242
+ "breaking": false,
243
+ "audience": "user",
244
+ "refs": [
245
+ {
246
+ "label": "#115",
247
+ "url": null
248
+ }
249
+ ]
250
+ }
251
+ ]
252
+ },
253
+ {
254
+ "version": "4.1.3",
255
+ "date": "2026-03-18",
256
+ "entries": [
257
+ {
258
+ "id": "01-acex-node-config-diff-apply-node-id-push",
259
+ "kind": "added",
260
+ "title": "`acex node config diff apply NODE_ID` pushes the planned changes to a device",
261
+ "description": "It works out the difference between observed and desired configuration, prints the device commands in a box and asks \"Are you sure you want to apply the config above?\". On yes it sends them over the node's first management connection; on no it exits without changes (exit status 0).",
262
+ "breaking": false,
263
+ "audience": "user",
264
+ "refs": [
265
+ {
266
+ "label": "#72",
267
+ "url": null
268
+ }
269
+ ]
270
+ },
271
+ {
272
+ "id": "03-acex-node-config-diff-plan-f-commands-pr",
273
+ "kind": "changed",
274
+ "title": "`acex node config diff plan -f commands` prints driver-rendered patch commands",
275
+ "description": "The `commands` format now asks the device driver to render the patch for the node, the same text that `diff apply` shows before asking for confirmation.",
276
+ "breaking": false,
277
+ "audience": "user",
278
+ "refs": [
279
+ {
280
+ "label": "#72",
281
+ "url": null
282
+ }
283
+ ]
284
+ },
285
+ {
286
+ "id": "02-acex-node-config-diff-plan-shows-changes",
287
+ "kind": "changed",
288
+ "title": "`acex node config diff plan` shows changes per component and attribute",
289
+ "description": "The `tree`, `compact`, `flat` and `summary` views now list added, removed and changed components, with before and after values per changed attribute; `flat` is a table of Type, Component, Attribute, Before and After. Output from earlier versions looked different, so scripts that parse it need to be checked.",
290
+ "breaking": false,
291
+ "audience": "user",
292
+ "refs": [
293
+ {
294
+ "label": "#70",
295
+ "url": null
296
+ },
297
+ {
298
+ "label": "#81",
299
+ "url": null
300
+ }
301
+ ]
302
+ }
303
+ ]
304
+ },
305
+ {
306
+ "version": "4.1.0",
307
+ "date": "2026-02-13",
308
+ "entries": [
309
+ {
310
+ "id": "03-acex-node-config-diff-plan-node-id-shows",
311
+ "kind": "added",
312
+ "title": "`acex node config diff plan NODE_ID` shows what would change on a node",
313
+ "description": "Compares the latest observed configuration with the desired one. `--format` (`-f`) selects `tree` (default), `compact`, `flat`, `summary` or `json`; `--max-depth` (`-d`) and `--no-values` shorten the tree view. A `commands` format is also accepted, but is not listed in the help; it appears to print device commands.",
314
+ "breaking": false,
315
+ "audience": "user",
316
+ "refs": [
317
+ {
318
+ "label": "#49",
319
+ "url": null
320
+ },
321
+ {
322
+ "label": "#52",
323
+ "url": null
324
+ }
325
+ ]
326
+ },
327
+ {
328
+ "id": "01-acex-node-config-show-desired-node-id-pr",
329
+ "kind": "added",
330
+ "title": "`acex node config show desired NODE_ID` prints the desired configuration",
331
+ "description": "Prints the configuration defined for the node as JSON, or as device configuration text with `--render`. A `--path` option is accepted but has no effect yet.",
332
+ "breaking": false,
333
+ "audience": "user",
334
+ "refs": [
335
+ {
336
+ "label": "#48",
337
+ "url": null
338
+ },
339
+ {
340
+ "label": "#49",
341
+ "url": null
342
+ }
343
+ ]
344
+ },
345
+ {
346
+ "id": "02-acex-node-config-show-observed-node-id-p",
347
+ "kind": "added",
348
+ "title": "`acex node config show observed NODE_ID` prints the latest backed-up config",
349
+ "description": "Prints the last configuration read from the device as parsed JSON, or as device configuration text with `--render`. Prints \"No observed config found.\" when there is none.",
350
+ "breaking": false,
351
+ "audience": "user",
352
+ "refs": [
353
+ {
354
+ "label": "#48",
355
+ "url": null
356
+ },
357
+ {
358
+ "label": "#49",
359
+ "url": null
360
+ }
361
+ ]
362
+ },
363
+ {
364
+ "id": "04-acex-starts-again-after-the-start-up-fai",
365
+ "kind": "fixed",
366
+ "title": "`acex` starts again after the start-up failure in 0.2.5",
367
+ "description": "The undefined option type in the node commands is gone, so the `acex` command loads normally.",
368
+ "breaking": false,
369
+ "audience": "user",
370
+ "refs": [
371
+ {
372
+ "label": "#47",
373
+ "url": null
374
+ }
375
+ ]
376
+ }
377
+ ]
378
+ },
379
+ {
380
+ "version": "0.2.5",
381
+ "date": "2026-02-10",
382
+ "entries": [
383
+ {
384
+ "id": "02-acex-node-config-observed-node-id-prints",
385
+ "kind": "added",
386
+ "title": "`acex node config observed NODE_ID` prints the latest backed-up configuration",
387
+ "description": "Prints the last configuration stored for a node, as device text by default or as JSON with `--format json` (`-f`), or a specific backup with `--hash`. In this release it cannot be used, because `acex` itself fails to start (see the next entry).",
388
+ "breaking": false,
389
+ "audience": "user",
390
+ "refs": [
391
+ {
392
+ "label": "#44",
393
+ "url": null
394
+ }
395
+ ]
396
+ },
397
+ {
398
+ "id": "01-clear-message-when-the-acex-api-cannot-b",
399
+ "kind": "added",
400
+ "title": "Clear message when the ACEX API cannot be reached",
401
+ "description": "Before running a command, `acex` now contacts the API address of the active context. If the connection fails it prints \"Could not connect to API!\" plus the reason, and for a certificate problem \"Could not connect to API due to untrusted certificate!\"; it then stops with exit status 0, so scripts cannot detect the failure from the status.",
402
+ "breaking": false,
403
+ "audience": "user",
404
+ "refs": [
405
+ {
406
+ "label": "#44",
407
+ "url": null
408
+ }
409
+ ]
410
+ }
411
+ ]
412
+ },
413
+ {
414
+ "version": "0.2.1",
415
+ "date": "2026-01-29",
416
+ "entries": [
417
+ {
418
+ "id": "03-placeholder-commands-acex-run-acex-list-",
419
+ "kind": "removed",
420
+ "title": "Placeholder commands `acex run`, `acex list` and `acex status` are gone",
421
+ "description": "These three commands did nothing but print a message and have been removed. Use `acex node list`, `acex asset list` and the `acex context` commands instead.",
422
+ "breaking": false,
423
+ "audience": "user",
424
+ "refs": [
425
+ {
426
+ "label": "#35",
427
+ "url": null
428
+ }
429
+ ]
430
+ },
431
+ {
432
+ "id": "02-choose-which-acex-api-to-talk-to-with-ac",
433
+ "kind": "added",
434
+ "title": "Choose which ACEX API to talk to with `acex context`",
435
+ "description": "`acex context add --name NAME --url URL` saves and activates an API address (`--no-verify-ssl` skips certificate checking). `acex context list`, `use NAME`, `show` and `verify-ssl true|false` manage saved contexts, which are kept in `~/.acex/.acex_cli_context.json`. Without an active context, node and asset commands report that none is selected.",
436
+ "breaking": false,
437
+ "audience": "user",
438
+ "refs": [
439
+ {
440
+ "label": "#35",
441
+ "url": null
442
+ }
443
+ ]
444
+ },
445
+ {
446
+ "id": "01-list-and-show-nodes-and-assets-with-acex",
447
+ "kind": "added",
448
+ "title": "List and show nodes and assets with `acex node` and `acex asset`",
449
+ "description": "`acex node list`, `acex node show NODE_ID`, `acex asset list` and `acex asset show ASSET_ID` fetch data from the ACEX API and print it as a table (one row per item, or a Field/Value table for `show`). An empty list prints \"No nodes found.\" or \"No assets found.\".",
450
+ "breaking": false,
451
+ "audience": "user",
452
+ "refs": [
453
+ {
454
+ "label": "#35",
455
+ "url": null
456
+ }
457
+ ]
458
+ },
459
+ {
460
+ "id": "04-shell-completion-is-enabled-for-acex",
461
+ "kind": "changed",
462
+ "title": "Shell completion is enabled for `acex`",
463
+ "description": "The command now offers the standard `--install-completion` and `--show-completion` options. Running `acex` with no sub-command prints the help text.",
464
+ "breaking": false,
465
+ "audience": "user",
466
+ "refs": [
467
+ {
468
+ "label": "#35",
469
+ "url": null
470
+ }
471
+ ]
472
+ }
473
+ ]
474
+ },
475
+ {
476
+ "version": "0.2.0",
477
+ "date": "2025-11-04",
478
+ "entries": [
479
+ {
480
+ "id": "01-first-acex-command-with-version-run-list",
481
+ "kind": "added",
482
+ "title": "First `acex` command with `version`, `run`, `list` and `status`",
483
+ "description": "The `acex` command exists, with `acex version`, `acex run FILE`, `acex list` and `acex status`.",
484
+ "breaking": false,
485
+ "audience": "user",
486
+ "refs": [
487
+ {
488
+ "label": "83efd0b",
489
+ "url": null
490
+ }
491
+ ]
492
+ }
493
+ ]
494
+ }
495
+ ],
496
+ "coming": [],
497
+ "highlights": []
498
+ }
@@ -45,7 +45,7 @@ def _decode_config_content(content: str) -> str:
45
45
 
46
46
 
47
47
  def _fetch_node_instance(sdk, identifier: str):
48
- """Resolve identifier to full NodeResponse. Used by show/apply that need asset info."""
48
+ """Resolve identifier to full NodeInstance. Used by show/apply that need asset info."""
49
49
  if identifier.isdigit():
50
50
  node = sdk.node_instances.get(identifier)
51
51
  else:
@@ -177,7 +177,7 @@ def _fetch_desired_config(sdk, identifier: str):
177
177
  typer.echo("No desired configuration found.")
178
178
  raise typer.Exit(1)
179
179
 
180
- # Return quick node + config. Config commands that need full NodeResponse
180
+ # Return quick node + config. Config commands that need full NodeInstance
181
181
  # (e.g. render, apply) will call _fetch_node_instance separately.
182
182
  return quick, config
183
183
 
@@ -638,7 +638,7 @@ def _print_config_section(console, data: dict, path: str):
638
638
 
639
639
 
640
640
  def _flatten_node(node, node_id: str = None) -> dict:
641
- """Flatten NodeResponse into a single-level dict for display."""
641
+ """Flatten NodeInstance into a single-level dict for display."""
642
642
  data = node.model_dump() if hasattr(node, "model_dump") else dict(node)
643
643
  asset = data.pop("asset", {}) or {}
644
644
  logical_node = data.pop("logical_node", {}) or {}
@@ -666,7 +666,7 @@ def _flatten_node(node, node_id: str = None) -> dict:
666
666
 
667
667
 
668
668
  def _resolve_node(sdk, identifier: str):
669
- """Resolve a node by ID or hostname. Returns NodeResponse or exits."""
669
+ """Resolve a node by ID or hostname. Returns NodeInstance or exits."""
670
670
  # Try as numeric ID first
671
671
  if identifier.isdigit():
672
672
  node = sdk.node_instances.get(identifier)
File without changes
File without changes
File without changes