systemlink-cli 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slcli/__init__.py +1 -0
- slcli/__main__.py +23 -0
- slcli/_version.py +4 -0
- slcli/asset_click.py +1289 -0
- slcli/cli_formatters.py +218 -0
- slcli/cli_utils.py +504 -0
- slcli/comment_click.py +602 -0
- slcli/completion_click.py +418 -0
- slcli/config.py +81 -0
- slcli/config_click.py +498 -0
- slcli/dff_click.py +979 -0
- slcli/dff_decorators.py +24 -0
- slcli/example_click.py +404 -0
- slcli/example_loader.py +274 -0
- slcli/example_provisioner.py +2777 -0
- slcli/examples/README.md +134 -0
- slcli/examples/_schema/schema-v1.0.json +169 -0
- slcli/examples/demo-complete-workflow/README.md +323 -0
- slcli/examples/demo-complete-workflow/config.yaml +638 -0
- slcli/examples/demo-test-plans/README.md +132 -0
- slcli/examples/demo-test-plans/config.yaml +154 -0
- slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
- slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
- slcli/examples/exercise-7-1-test-plans/README.md +93 -0
- slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
- slcli/examples/spec-compliance-notebooks/README.md +140 -0
- slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
- slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- slcli/feed_click.py +892 -0
- slcli/file_click.py +932 -0
- slcli/function_click.py +1400 -0
- slcli/function_templates.py +85 -0
- slcli/main.py +406 -0
- slcli/mcp_click.py +269 -0
- slcli/mcp_server.py +748 -0
- slcli/notebook_click.py +1770 -0
- slcli/platform.py +345 -0
- slcli/policy_click.py +679 -0
- slcli/policy_utils.py +411 -0
- slcli/profiles.py +411 -0
- slcli/response_handlers.py +359 -0
- slcli/routine_click.py +763 -0
- slcli/skill_click.py +253 -0
- slcli/skills/slcli/SKILL.md +713 -0
- slcli/skills/slcli/references/analysis-recipes.md +474 -0
- slcli/skills/slcli/references/filtering.md +236 -0
- slcli/skills/systemlink-webapp/SKILL.md +744 -0
- slcli/skills/systemlink-webapp/references/deployment.md +123 -0
- slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
- slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
- slcli/ssl_trust.py +93 -0
- slcli/system_click.py +2216 -0
- slcli/table_utils.py +124 -0
- slcli/tag_click.py +794 -0
- slcli/templates_click.py +599 -0
- slcli/testmonitor_click.py +1667 -0
- slcli/universal_handlers.py +305 -0
- slcli/user_click.py +1218 -0
- slcli/utils.py +832 -0
- slcli/web_editor.py +295 -0
- slcli/webapp_click.py +981 -0
- slcli/workflow_preview.py +287 -0
- slcli/workflows_click.py +988 -0
- slcli/workitem_click.py +2258 -0
- slcli/workspace_click.py +576 -0
- slcli/workspace_utils.py +206 -0
- systemlink_cli-1.3.1.dist-info/METADATA +20 -0
- systemlink_cli-1.3.1.dist-info/RECORD +74 -0
- systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
- systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
- systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
slcli/workspace_click.py
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"""CLI commands for managing SystemLink workspaces."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, Dict, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from .cli_utils import validate_output_format
|
|
10
|
+
from .utils import (
|
|
11
|
+
ExitCodes,
|
|
12
|
+
format_success,
|
|
13
|
+
get_base_url,
|
|
14
|
+
handle_api_error,
|
|
15
|
+
make_api_request,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _fetch_workspaces_page(
|
|
20
|
+
name_filter: Optional[str] = None, take: int = 25, skip: int = 0
|
|
21
|
+
) -> Tuple[list, int, Optional[str]]:
|
|
22
|
+
"""Fetch a single page of workspaces with optional server-side filtering.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name_filter: Optional filter pattern for workspace name (uses *TEXT* format
|
|
26
|
+
for case-insensitive substring matching)
|
|
27
|
+
take: Number of items to fetch (max 100)
|
|
28
|
+
skip: Number of items to skip
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (workspaces_list, total_count, error_message).
|
|
32
|
+
Error message is None if successful.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
url = f"{get_base_url()}/niuser/v1/workspaces"
|
|
36
|
+
page_size = min(take, 100) # API max take is 100
|
|
37
|
+
|
|
38
|
+
query_params = [f"take={page_size}", f"skip={skip}"]
|
|
39
|
+
if name_filter:
|
|
40
|
+
# Use *TEXT* pattern for case-insensitive substring matching
|
|
41
|
+
query_params.append(f"name=*{name_filter}*")
|
|
42
|
+
|
|
43
|
+
paginated_url = url + "?" + "&".join(query_params)
|
|
44
|
+
|
|
45
|
+
resp = make_api_request("GET", paginated_url, payload=None)
|
|
46
|
+
data = resp.json()
|
|
47
|
+
|
|
48
|
+
workspaces = data.get("workspaces", [])
|
|
49
|
+
total_count = data.get("totalCount", 0)
|
|
50
|
+
|
|
51
|
+
return workspaces, total_count, None
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
return [], 0, f"Failed to fetch workspaces: {str(exc)}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def register_workspace_commands(cli: Any) -> None:
|
|
57
|
+
"""Register the 'workspace' command group and its subcommands."""
|
|
58
|
+
|
|
59
|
+
@cli.group()
|
|
60
|
+
def workspace() -> None:
|
|
61
|
+
"""Manage workspaces."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
@workspace.command(name="list")
|
|
65
|
+
@click.option(
|
|
66
|
+
"--format",
|
|
67
|
+
"-f",
|
|
68
|
+
type=click.Choice(["table", "json"]),
|
|
69
|
+
default="table",
|
|
70
|
+
show_default=True,
|
|
71
|
+
help="Output format",
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"--include-disabled",
|
|
75
|
+
is_flag=True,
|
|
76
|
+
help="Include disabled workspaces in the results",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--filter",
|
|
80
|
+
"name_filter",
|
|
81
|
+
help="Filter by workspace name (case-insensitive substring match)",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--take",
|
|
85
|
+
"-t",
|
|
86
|
+
type=int,
|
|
87
|
+
default=25,
|
|
88
|
+
show_default=True,
|
|
89
|
+
help="Maximum number of workspaces to return from API",
|
|
90
|
+
)
|
|
91
|
+
def list_workspaces(
|
|
92
|
+
format: str = "table",
|
|
93
|
+
include_disabled: bool = False,
|
|
94
|
+
name_filter: Optional[str] = None,
|
|
95
|
+
take: int = 25,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""List workspaces with optional filtering.
|
|
98
|
+
|
|
99
|
+
The --filter option performs server-side case-insensitive substring
|
|
100
|
+
matching on workspace names. The --take option limits the number of
|
|
101
|
+
results shown per page (max 100).
|
|
102
|
+
"""
|
|
103
|
+
format_output = validate_output_format(format)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# For JSON format, respect --take and output without interactive pagination
|
|
107
|
+
if format_output.lower() == "json":
|
|
108
|
+
all_workspaces = []
|
|
109
|
+
skip = 0
|
|
110
|
+
remaining = take if take and take > 0 else 25
|
|
111
|
+
while remaining > 0:
|
|
112
|
+
page_take = min(remaining, 100)
|
|
113
|
+
workspaces, total_count, error = _fetch_workspaces_page(
|
|
114
|
+
name_filter, take=page_take, skip=skip
|
|
115
|
+
)
|
|
116
|
+
if error:
|
|
117
|
+
click.echo(f"✗ {error}", err=True)
|
|
118
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
119
|
+
|
|
120
|
+
# Filter by enabled status if needed
|
|
121
|
+
if not include_disabled:
|
|
122
|
+
workspaces = [ws for ws in workspaces if ws.get("enabled", True)]
|
|
123
|
+
|
|
124
|
+
all_workspaces.extend(workspaces)
|
|
125
|
+
|
|
126
|
+
# Stop when we've collected the requested amount or reached total
|
|
127
|
+
if len(all_workspaces) >= take or skip + page_take >= total_count:
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
skip += page_take
|
|
131
|
+
remaining = take - len(all_workspaces)
|
|
132
|
+
|
|
133
|
+
# Trim in case we over-collected due to page boundaries
|
|
134
|
+
if take:
|
|
135
|
+
all_workspaces = all_workspaces[:take]
|
|
136
|
+
click.echo(json.dumps(all_workspaces, indent=2))
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
# For table format, implement interactive lazy loading
|
|
140
|
+
skip = 0
|
|
141
|
+
total_count_from_api = 0
|
|
142
|
+
shown_count = 0
|
|
143
|
+
|
|
144
|
+
def workspace_formatter(workspace: dict) -> list:
|
|
145
|
+
enabled = "✓" if workspace.get("enabled", True) else "✗"
|
|
146
|
+
default = "✓" if workspace.get("default", False) else ""
|
|
147
|
+
return [workspace.get("name", "Unknown"), workspace.get("id", ""), enabled, default]
|
|
148
|
+
|
|
149
|
+
while True:
|
|
150
|
+
# Fetch next page
|
|
151
|
+
workspaces, total_count_from_api, error = _fetch_workspaces_page(
|
|
152
|
+
name_filter, take=take, skip=skip
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if error:
|
|
156
|
+
click.echo(f"✗ {error}", err=True)
|
|
157
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
158
|
+
|
|
159
|
+
# Filter by enabled status if needed
|
|
160
|
+
if not include_disabled:
|
|
161
|
+
workspaces = [ws for ws in workspaces if ws.get("enabled", True)]
|
|
162
|
+
|
|
163
|
+
if not workspaces and skip == 0:
|
|
164
|
+
click.echo("No workspaces found.")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
if not workspaces:
|
|
168
|
+
# No more results on this page
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
# Display the page
|
|
172
|
+
from .table_utils import output_formatted_list
|
|
173
|
+
|
|
174
|
+
output_formatted_list(
|
|
175
|
+
workspaces,
|
|
176
|
+
format_output,
|
|
177
|
+
["Name", "ID", "Enabled", "Default"],
|
|
178
|
+
[30, 36, 8, 8],
|
|
179
|
+
workspace_formatter,
|
|
180
|
+
"", # Empty message not needed here
|
|
181
|
+
"workspace(s)",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
shown_count += len(workspaces)
|
|
185
|
+
skip += take
|
|
186
|
+
|
|
187
|
+
# Check if there are potentially more results from the API
|
|
188
|
+
# We check skip against total_count to see if the next page exists
|
|
189
|
+
if skip >= total_count_from_api:
|
|
190
|
+
break
|
|
191
|
+
|
|
192
|
+
# Ask user if they want to see more; if non-interactive, stop
|
|
193
|
+
click.echo(f"\nShowing {shown_count} workspace(s) so far. More may be available.")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
is_tty = sys.stdout.isatty() and sys.stdin.isatty()
|
|
197
|
+
except Exception:
|
|
198
|
+
is_tty = False
|
|
199
|
+
|
|
200
|
+
if not is_tty:
|
|
201
|
+
break
|
|
202
|
+
if not click.confirm("Show next page?", default=True):
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
handle_api_error(exc)
|
|
207
|
+
|
|
208
|
+
@workspace.command(name="disable")
|
|
209
|
+
@click.option(
|
|
210
|
+
"--id",
|
|
211
|
+
"-i",
|
|
212
|
+
required=True,
|
|
213
|
+
help="ID of the workspace to disable",
|
|
214
|
+
)
|
|
215
|
+
@click.confirmation_option(prompt="Are you sure you want to disable this workspace?")
|
|
216
|
+
def disable_workspace(id: str) -> None:
|
|
217
|
+
"""Disable a workspace."""
|
|
218
|
+
from .utils import check_readonly_mode
|
|
219
|
+
|
|
220
|
+
check_readonly_mode("disable a workspace")
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# Get workspace info before disabling for confirmation
|
|
224
|
+
# Fetch all workspaces to find the target
|
|
225
|
+
all_workspaces = []
|
|
226
|
+
skip = 0
|
|
227
|
+
while True:
|
|
228
|
+
workspaces, total_count, error = _fetch_workspaces_page(take=100, skip=skip)
|
|
229
|
+
if error:
|
|
230
|
+
click.echo(f"✗ {error}", err=True)
|
|
231
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
232
|
+
|
|
233
|
+
all_workspaces.extend(workspaces)
|
|
234
|
+
|
|
235
|
+
if skip + 100 >= total_count:
|
|
236
|
+
break
|
|
237
|
+
skip += 100
|
|
238
|
+
|
|
239
|
+
# Find the workspace to get its details
|
|
240
|
+
workspace_to_disable = None
|
|
241
|
+
for ws in all_workspaces:
|
|
242
|
+
if ws.get("id") == id:
|
|
243
|
+
workspace_to_disable = ws
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
if not workspace_to_disable:
|
|
247
|
+
click.echo(f"✗ Workspace with ID '{id}' not found", err=True)
|
|
248
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
249
|
+
|
|
250
|
+
workspace_name = workspace_to_disable.get("name", id)
|
|
251
|
+
|
|
252
|
+
# Check if workspace is already disabled
|
|
253
|
+
if not workspace_to_disable.get("enabled", True):
|
|
254
|
+
click.echo(f"✗ Workspace '{workspace_name}' is already disabled", err=True)
|
|
255
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
256
|
+
|
|
257
|
+
# Update the workspace to disable it
|
|
258
|
+
update_url = f"{get_base_url()}/niuser/v1/workspaces/{id}"
|
|
259
|
+
update_payload = {"name": workspace_name, "enabled": False}
|
|
260
|
+
|
|
261
|
+
make_api_request("PUT", update_url, update_payload)
|
|
262
|
+
|
|
263
|
+
format_success(
|
|
264
|
+
f"Workspace '{workspace_name}' disabled successfully",
|
|
265
|
+
{"id": id, "name": workspace_name, "enabled": False},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
handle_api_error(exc)
|
|
270
|
+
|
|
271
|
+
@workspace.command(name="get")
|
|
272
|
+
@click.option(
|
|
273
|
+
"--workspace",
|
|
274
|
+
"-w",
|
|
275
|
+
required=True,
|
|
276
|
+
help="Workspace name or ID",
|
|
277
|
+
)
|
|
278
|
+
@click.option(
|
|
279
|
+
"--format",
|
|
280
|
+
"-f",
|
|
281
|
+
type=click.Choice(["table", "json"]),
|
|
282
|
+
default="table",
|
|
283
|
+
show_default=True,
|
|
284
|
+
help="Output format",
|
|
285
|
+
)
|
|
286
|
+
def get_workspace(workspace: str, format: str) -> None:
|
|
287
|
+
"""Show workspace details and contents."""
|
|
288
|
+
try:
|
|
289
|
+
# Get workspace info - fetch all pages until we find the workspace
|
|
290
|
+
all_workspaces = []
|
|
291
|
+
skip = 0
|
|
292
|
+
while True:
|
|
293
|
+
workspaces, total_count, error = _fetch_workspaces_page(take=100, skip=skip)
|
|
294
|
+
if error:
|
|
295
|
+
click.echo(f"✗ {error}", err=True)
|
|
296
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
297
|
+
|
|
298
|
+
all_workspaces.extend(workspaces)
|
|
299
|
+
|
|
300
|
+
if skip + 100 >= total_count:
|
|
301
|
+
break
|
|
302
|
+
skip += 100
|
|
303
|
+
|
|
304
|
+
# Find the workspace by ID or name
|
|
305
|
+
target_workspace = None
|
|
306
|
+
target_workspace = next(
|
|
307
|
+
(
|
|
308
|
+
ws
|
|
309
|
+
for ws in all_workspaces
|
|
310
|
+
if ws.get("id") == workspace or ws.get("name") == workspace
|
|
311
|
+
),
|
|
312
|
+
None,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
if not target_workspace:
|
|
316
|
+
click.echo(f"✗ Workspace '{workspace}' not found", err=True)
|
|
317
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
318
|
+
|
|
319
|
+
workspace_id = target_workspace.get("id")
|
|
320
|
+
workspace_name = target_workspace.get("name")
|
|
321
|
+
|
|
322
|
+
# Get workspace contents with error handling
|
|
323
|
+
templates, templates_error = _get_workspace_templates(workspace_id)
|
|
324
|
+
workflows, workflows_error = _get_workspace_workflows(workspace_id)
|
|
325
|
+
notebooks, notebooks_error = _get_workspace_notebooks(workspace_id)
|
|
326
|
+
|
|
327
|
+
# Prepare error information
|
|
328
|
+
access_errors = {}
|
|
329
|
+
if templates_error:
|
|
330
|
+
access_errors["templates"] = templates_error
|
|
331
|
+
if workflows_error:
|
|
332
|
+
access_errors["workflows"] = workflows_error
|
|
333
|
+
if notebooks_error:
|
|
334
|
+
access_errors["notebooks"] = notebooks_error
|
|
335
|
+
|
|
336
|
+
workspace_info = {
|
|
337
|
+
"workspace": {
|
|
338
|
+
"id": workspace_id,
|
|
339
|
+
"name": workspace_name,
|
|
340
|
+
"enabled": target_workspace.get("enabled", True),
|
|
341
|
+
"default": target_workspace.get("default", False),
|
|
342
|
+
},
|
|
343
|
+
"contents": {
|
|
344
|
+
"templates": templates,
|
|
345
|
+
"workflows": workflows,
|
|
346
|
+
"notebooks": notebooks,
|
|
347
|
+
},
|
|
348
|
+
"summary": {
|
|
349
|
+
"total_templates": len(templates),
|
|
350
|
+
"total_workflows": len(workflows),
|
|
351
|
+
"total_notebooks": len(notebooks),
|
|
352
|
+
},
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Add access errors to JSON output if any exist
|
|
356
|
+
if access_errors:
|
|
357
|
+
workspace_info["access_errors"] = access_errors
|
|
358
|
+
|
|
359
|
+
if format == "json":
|
|
360
|
+
click.echo(json.dumps(workspace_info, indent=2))
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
# Table format
|
|
364
|
+
click.echo(f"Workspace Information: {workspace_name}")
|
|
365
|
+
click.echo("=" * 50)
|
|
366
|
+
click.echo(f"ID: {workspace_id}")
|
|
367
|
+
click.echo(f"Name: {workspace_name}")
|
|
368
|
+
click.echo(f"Enabled: {'✓' if target_workspace.get('enabled', True) else '✗'}")
|
|
369
|
+
click.echo(f"Default: {'✓' if target_workspace.get('default', False) else '✗'}")
|
|
370
|
+
|
|
371
|
+
# Templates section
|
|
372
|
+
click.echo(f"\nTest Plan Templates ({len(templates)})")
|
|
373
|
+
click.echo("-" * 30)
|
|
374
|
+
if templates_error:
|
|
375
|
+
click.echo(f"✗ {templates_error}")
|
|
376
|
+
elif templates:
|
|
377
|
+
click.echo("┌" + "─" * 42 + "┬" + "─" * 38 + "┐")
|
|
378
|
+
click.echo(f"│ {'Name':<40} │ {'ID':<36} │")
|
|
379
|
+
click.echo("├" + "─" * 42 + "┼" + "─" * 38 + "┤")
|
|
380
|
+
for template in templates:
|
|
381
|
+
name = template.get("name", "")[:40]
|
|
382
|
+
template_id = template.get("id", "")[:36]
|
|
383
|
+
click.echo(f"│ {name:<40} │ {template_id:<36} │")
|
|
384
|
+
click.echo("└" + "─" * 42 + "┴" + "─" * 38 + "┘")
|
|
385
|
+
else:
|
|
386
|
+
click.echo("No test plan templates found.")
|
|
387
|
+
|
|
388
|
+
# Workflows section
|
|
389
|
+
click.echo(f"\nWorkflows ({len(workflows)})")
|
|
390
|
+
click.echo("-" * 30)
|
|
391
|
+
if workflows_error:
|
|
392
|
+
click.echo(f"✗ {workflows_error}")
|
|
393
|
+
elif workflows:
|
|
394
|
+
click.echo("┌" + "─" * 42 + "┬" + "─" * 38 + "┐")
|
|
395
|
+
click.echo(f"│ {'Name':<40} │ {'ID':<36} │")
|
|
396
|
+
click.echo("├" + "─" * 42 + "┼" + "─" * 38 + "┤")
|
|
397
|
+
for workflow in workflows:
|
|
398
|
+
name = workflow.get("name", "")[:40]
|
|
399
|
+
workflow_id = workflow.get("id", "")[:36]
|
|
400
|
+
click.echo(f"│ {name:<40} │ {workflow_id:<36} │")
|
|
401
|
+
click.echo("└" + "─" * 42 + "┴" + "─" * 38 + "┘")
|
|
402
|
+
else:
|
|
403
|
+
click.echo("No workflows found.")
|
|
404
|
+
|
|
405
|
+
# Notebooks section
|
|
406
|
+
click.echo(f"\nNotebooks ({len(notebooks)})")
|
|
407
|
+
click.echo("-" * 30)
|
|
408
|
+
if notebooks_error:
|
|
409
|
+
click.echo(f"✗ {notebooks_error}")
|
|
410
|
+
elif notebooks:
|
|
411
|
+
click.echo("┌" + "─" * 42 + "┬" + "─" * 38 + "┐")
|
|
412
|
+
click.echo(f"│ {'Name':<40} │ {'ID':<36} │")
|
|
413
|
+
click.echo("├" + "─" * 42 + "┼" + "─" * 38 + "┤")
|
|
414
|
+
for notebook in notebooks:
|
|
415
|
+
name = notebook.get("name", "")[:40]
|
|
416
|
+
notebook_id = notebook.get("id", "")[:36]
|
|
417
|
+
click.echo(f"│ {name:<40} │ {notebook_id:<36} │")
|
|
418
|
+
click.echo("└" + "─" * 42 + "┴" + "─" * 38 + "┘")
|
|
419
|
+
else:
|
|
420
|
+
click.echo("No notebooks found.")
|
|
421
|
+
|
|
422
|
+
except Exception as exc:
|
|
423
|
+
handle_api_error(exc)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _get_workspace_map() -> Dict[str, str]:
|
|
427
|
+
"""Get a mapping of workspace IDs to names.
|
|
428
|
+
|
|
429
|
+
Fetches all workspaces using pagination (max 100 per request).
|
|
430
|
+
"""
|
|
431
|
+
try:
|
|
432
|
+
workspace_map: Dict[str, str] = {}
|
|
433
|
+
skip = 0
|
|
434
|
+
page_size = 100 # API max take is 100
|
|
435
|
+
|
|
436
|
+
while True:
|
|
437
|
+
url = f"{get_base_url()}/niuser/v1/workspaces?take={page_size}&skip={skip}"
|
|
438
|
+
resp = make_api_request("GET", url)
|
|
439
|
+
data = resp.json()
|
|
440
|
+
workspaces = data.get("workspaces", [])
|
|
441
|
+
|
|
442
|
+
# Add workspaces from this page to the map
|
|
443
|
+
for ws in workspaces:
|
|
444
|
+
if ws.get("id"):
|
|
445
|
+
workspace_map[ws.get("id")] = ws.get("name")
|
|
446
|
+
|
|
447
|
+
# Check if we got all workspaces
|
|
448
|
+
total_count = data.get("totalCount", 0)
|
|
449
|
+
if skip + len(workspaces) >= total_count:
|
|
450
|
+
break
|
|
451
|
+
|
|
452
|
+
skip += page_size
|
|
453
|
+
|
|
454
|
+
return workspace_map
|
|
455
|
+
except Exception:
|
|
456
|
+
return {}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _get_workspace_templates(workspace_id: str) -> Tuple[list, Optional[str]]:
|
|
460
|
+
"""Get test plan templates in a workspace using continuation token pagination.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Tuple of (templates_list, error_message). If error_message is not None,
|
|
464
|
+
it indicates an access or permission issue.
|
|
465
|
+
"""
|
|
466
|
+
try:
|
|
467
|
+
url = f"{get_base_url()}/niworkorder/v1/query-testplan-templates"
|
|
468
|
+
all_templates = []
|
|
469
|
+
continuation_token = None
|
|
470
|
+
|
|
471
|
+
while True:
|
|
472
|
+
payload = {
|
|
473
|
+
"take": 100, # Use smaller page size for efficient pagination
|
|
474
|
+
"projection": ["ID", "NAME"],
|
|
475
|
+
"filter": f'workspace == "{workspace_id}"',
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if continuation_token:
|
|
479
|
+
payload["continuationToken"] = continuation_token
|
|
480
|
+
|
|
481
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
482
|
+
data = resp.json()
|
|
483
|
+
|
|
484
|
+
templates = data.get("testPlanTemplates", [])
|
|
485
|
+
all_templates.extend(templates)
|
|
486
|
+
|
|
487
|
+
# Check if there are more pages
|
|
488
|
+
continuation_token = data.get("continuationToken")
|
|
489
|
+
if not continuation_token:
|
|
490
|
+
break
|
|
491
|
+
|
|
492
|
+
return all_templates, None
|
|
493
|
+
except Exception as exc:
|
|
494
|
+
error_msg = str(exc).lower()
|
|
495
|
+
if "401" in error_msg or "unauthorized" in error_msg or "permission" in error_msg:
|
|
496
|
+
return [], "Access denied (insufficient permissions)"
|
|
497
|
+
elif "403" in error_msg or "forbidden" in error_msg:
|
|
498
|
+
return [], "Access forbidden"
|
|
499
|
+
elif "404" in error_msg or "not found" in error_msg:
|
|
500
|
+
return [], "Service not available"
|
|
501
|
+
else:
|
|
502
|
+
return [], f"Unable to retrieve templates: {str(exc)}"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _get_workspace_workflows(workspace_id: str) -> Tuple[list, Optional[str]]:
|
|
506
|
+
"""Get workflows in a workspace using continuation token pagination.
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Tuple of (workflows_list, error_message). If error_message is not None,
|
|
510
|
+
it indicates an access or permission issue.
|
|
511
|
+
"""
|
|
512
|
+
try:
|
|
513
|
+
url = f"{get_base_url()}/niworkorder/v1/query-workflows?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
514
|
+
all_workflows = []
|
|
515
|
+
continuation_token = None
|
|
516
|
+
|
|
517
|
+
while True:
|
|
518
|
+
payload = {
|
|
519
|
+
"take": 100, # Use smaller page size for efficient pagination
|
|
520
|
+
"projection": ["ID", "NAME", "WORKSPACE"],
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if continuation_token:
|
|
524
|
+
payload["continuationToken"] = continuation_token
|
|
525
|
+
|
|
526
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
527
|
+
data = resp.json()
|
|
528
|
+
|
|
529
|
+
workflows = data.get("workflows", [])
|
|
530
|
+
all_workflows.extend(workflows)
|
|
531
|
+
|
|
532
|
+
# Check if there are more pages
|
|
533
|
+
continuation_token = data.get("continuationToken")
|
|
534
|
+
if not continuation_token:
|
|
535
|
+
break
|
|
536
|
+
|
|
537
|
+
# Filter workflows by workspace since the API doesn't support workspace filtering
|
|
538
|
+
workspace_workflows = [wf for wf in all_workflows if wf.get("workspace") == workspace_id]
|
|
539
|
+
return workspace_workflows, None
|
|
540
|
+
except Exception as exc:
|
|
541
|
+
error_msg = str(exc).lower()
|
|
542
|
+
if "401" in error_msg or "unauthorized" in error_msg or "permission" in error_msg:
|
|
543
|
+
return [], "Access denied (insufficient permissions)"
|
|
544
|
+
elif "403" in error_msg or "forbidden" in error_msg:
|
|
545
|
+
return [], "Access forbidden"
|
|
546
|
+
elif "404" in error_msg or "not found" in error_msg:
|
|
547
|
+
return [], "Service not available"
|
|
548
|
+
else:
|
|
549
|
+
return [], f"Unable to retrieve workflows: {str(exc)}"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _get_workspace_notebooks(workspace_id: str) -> Tuple[list, Optional[str]]:
|
|
553
|
+
"""Get notebooks in a workspace.
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
Tuple of (notebooks_list, error_message). If error_message is not None,
|
|
557
|
+
it indicates an access or permission issue.
|
|
558
|
+
"""
|
|
559
|
+
try:
|
|
560
|
+
url = f"{get_base_url()}/ninotebook/v1/notebook/query"
|
|
561
|
+
payload = {"take": 1000, "filter": f'workspace = "{workspace_id}"'}
|
|
562
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
563
|
+
data = resp.json()
|
|
564
|
+
notebooks = data.get("notebooks", [])
|
|
565
|
+
# Convert to consistent format
|
|
566
|
+
return [{"id": nb.get("id"), "name": nb.get("name")} for nb in notebooks], None
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
error_msg = str(exc).lower()
|
|
569
|
+
if "401" in error_msg or "unauthorized" in error_msg or "permission" in error_msg:
|
|
570
|
+
return [], "Access denied (insufficient permissions)"
|
|
571
|
+
elif "403" in error_msg or "forbidden" in error_msg:
|
|
572
|
+
return [], "Access forbidden"
|
|
573
|
+
elif "404" in error_msg or "not found" in error_msg:
|
|
574
|
+
return [], "Service not available"
|
|
575
|
+
else:
|
|
576
|
+
return [], f"Unable to retrieve notebooks: {str(exc)}"
|