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/workflows_click.py
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
1
|
+
"""CLI commands for managing SystemLink workflows."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import webbrowser
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import questionary
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from . import workflow_preview
|
|
16
|
+
from .cli_utils import validate_output_format
|
|
17
|
+
from .platform import require_feature
|
|
18
|
+
from .universal_handlers import UniversalResponseHandler, FilteredResponse
|
|
19
|
+
from .utils import (
|
|
20
|
+
display_api_errors,
|
|
21
|
+
ExitCodes,
|
|
22
|
+
extract_error_type,
|
|
23
|
+
get_base_url,
|
|
24
|
+
get_workspace_id_with_fallback,
|
|
25
|
+
get_workspace_map,
|
|
26
|
+
handle_api_error,
|
|
27
|
+
load_json_file,
|
|
28
|
+
make_api_request,
|
|
29
|
+
sanitize_filename,
|
|
30
|
+
save_json_file,
|
|
31
|
+
)
|
|
32
|
+
from .workspace_utils import (
|
|
33
|
+
get_effective_workspace,
|
|
34
|
+
get_workspace_display_name,
|
|
35
|
+
resolve_workspace_filter,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
"""Workflow CLI commands.
|
|
39
|
+
|
|
40
|
+
Preview (Mermaid diagram) generation helpers live in
|
|
41
|
+
`slcli.workflow_preview`. Deprecated internal wrapper functions have been
|
|
42
|
+
removed; import the public helpers directly from that module.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _query_all_workflows(
|
|
47
|
+
workspace_filter: Optional[str] = None, workspace_map: Optional[dict] = None
|
|
48
|
+
) -> List[Dict[str, Any]]:
|
|
49
|
+
"""Query all workflows using continuation token pagination.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
workspace_filter: Optional workspace ID to filter by
|
|
53
|
+
workspace_map: Optional workspace mapping to avoid repeated lookups
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of all workflows, optionally filtered by workspace
|
|
57
|
+
"""
|
|
58
|
+
url = f"{get_base_url()}/niworkorder/v1/query-workflows?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
59
|
+
all_workflows = []
|
|
60
|
+
continuation_token = None
|
|
61
|
+
|
|
62
|
+
while True:
|
|
63
|
+
# Build payload for the request
|
|
64
|
+
payload: Dict[str, Union[int, str]] = {
|
|
65
|
+
"take": 100, # Use smaller page size for efficient pagination
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Add workspace filter if specified
|
|
69
|
+
if workspace_filter:
|
|
70
|
+
payload["filter"] = f'WORKSPACE == "{workspace_filter}"'
|
|
71
|
+
|
|
72
|
+
# Add continuation token if we have one
|
|
73
|
+
if continuation_token:
|
|
74
|
+
payload["continuationToken"] = continuation_token
|
|
75
|
+
|
|
76
|
+
resp = make_api_request("POST", url, payload)
|
|
77
|
+
data = resp.json()
|
|
78
|
+
|
|
79
|
+
# Extract workflows from this page
|
|
80
|
+
workflows = data.get("workflows", [])
|
|
81
|
+
all_workflows.extend(workflows)
|
|
82
|
+
|
|
83
|
+
# Check if there are more pages
|
|
84
|
+
continuation_token = data.get("continuationToken")
|
|
85
|
+
if not continuation_token:
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
return all_workflows
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def register_workflows_commands(cli: Any) -> None:
|
|
92
|
+
"""Register the 'workflow' command group and its subcommands."""
|
|
93
|
+
|
|
94
|
+
@cli.group()
|
|
95
|
+
@click.pass_context
|
|
96
|
+
def workflow(ctx: click.Context) -> None:
|
|
97
|
+
"""Manage workflows (init, import/export, update)."""
|
|
98
|
+
# Check for platform feature availability
|
|
99
|
+
# Only check if a subcommand is being invoked (not just --help)
|
|
100
|
+
if ctx.invoked_subcommand is not None:
|
|
101
|
+
require_feature("workflows")
|
|
102
|
+
|
|
103
|
+
@workflow.command(name="init")
|
|
104
|
+
@click.option(
|
|
105
|
+
"--name",
|
|
106
|
+
"-n",
|
|
107
|
+
help="Workflow name (will prompt if not provided)",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--description",
|
|
111
|
+
"-d",
|
|
112
|
+
help="Workflow description (will prompt if not provided)",
|
|
113
|
+
)
|
|
114
|
+
@click.option(
|
|
115
|
+
"--workspace",
|
|
116
|
+
"-w",
|
|
117
|
+
default="Default",
|
|
118
|
+
help="Workspace name or ID (default: 'Default')",
|
|
119
|
+
)
|
|
120
|
+
@click.option(
|
|
121
|
+
"--output",
|
|
122
|
+
"-o",
|
|
123
|
+
help="Output file path (default: <name>-workflow.json)",
|
|
124
|
+
)
|
|
125
|
+
def init_workflow(
|
|
126
|
+
name: Optional[str], description: Optional[str], workspace: str, output: Optional[str]
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Create a workflow JSON skeleton.
|
|
129
|
+
|
|
130
|
+
Creates a workflow JSON file with the required schema structure.
|
|
131
|
+
Name and description are recommended fields. Workspace is required
|
|
132
|
+
and defaults to 'Default' if not specified.
|
|
133
|
+
"""
|
|
134
|
+
# Prompt for required fields if not provided
|
|
135
|
+
if not name:
|
|
136
|
+
name = click.prompt("Workflow name", type=str)
|
|
137
|
+
if not description:
|
|
138
|
+
description = click.prompt("Workflow description", type=str, default="")
|
|
139
|
+
|
|
140
|
+
# Generate output filename if not provided
|
|
141
|
+
if not output:
|
|
142
|
+
assert name is not None # Should be set by prompt above
|
|
143
|
+
safe_name = sanitize_filename(name, "workflow")
|
|
144
|
+
output = f"{safe_name}-workflow.json"
|
|
145
|
+
|
|
146
|
+
# Resolve workspace name to ID
|
|
147
|
+
try:
|
|
148
|
+
workspace = get_effective_workspace(workspace) or workspace
|
|
149
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
click.echo(f"✗ Error resolving workspace '{workspace}': {exc}", err=True)
|
|
152
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
153
|
+
|
|
154
|
+
workflow_data = {
|
|
155
|
+
"name": name,
|
|
156
|
+
"description": description,
|
|
157
|
+
"workspace": workspace_id,
|
|
158
|
+
"actions": [
|
|
159
|
+
{
|
|
160
|
+
"name": "START",
|
|
161
|
+
"displayText": "Start",
|
|
162
|
+
"privilegeSpecificity": ["ExecuteTest"],
|
|
163
|
+
"executionAction": {"type": "MANUAL", "action": "START"},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"name": "COMPLETE",
|
|
167
|
+
"displayText": "Complete",
|
|
168
|
+
"privilegeSpecificity": ["Close"],
|
|
169
|
+
"executionAction": {"type": "MANUAL", "action": "COMPLETE"},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "RUN_NOTEBOOK",
|
|
173
|
+
"displayText": "Run Notebook",
|
|
174
|
+
"iconClass": None,
|
|
175
|
+
"i18n": [],
|
|
176
|
+
"privilegeSpecificity": ["ExecuteTest"],
|
|
177
|
+
"executionAction": {
|
|
178
|
+
"action": "RUN_NOTEBOOK",
|
|
179
|
+
"type": "NOTEBOOK",
|
|
180
|
+
"notebookId": "00000000-0000-0000-0000-000000000000",
|
|
181
|
+
"parameters": {
|
|
182
|
+
"partNumber": "<partNumber>",
|
|
183
|
+
"dut": "<dutId>",
|
|
184
|
+
"operator": "<assignedTo>",
|
|
185
|
+
"testProgram": "<testProgram>",
|
|
186
|
+
"location": "<properties.region>-<properties.facility>-<properties.lab>",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "PLAN_SCHEDULE",
|
|
192
|
+
"displayText": "Schedule Test Plan",
|
|
193
|
+
"iconClass": "SCHEDULE",
|
|
194
|
+
"i18n": [],
|
|
195
|
+
"privilegeSpecificity": [],
|
|
196
|
+
"executionAction": {"action": "PLAN_SCHEDULE", "type": "SCHEDULE"},
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"name": "RUN_JOB",
|
|
200
|
+
"displayText": "Run Job",
|
|
201
|
+
"iconClass": "DEPLOY",
|
|
202
|
+
"i18n": [],
|
|
203
|
+
"privilegeSpecificity": [],
|
|
204
|
+
"executionAction": {
|
|
205
|
+
"action": "RUN_JOB",
|
|
206
|
+
"type": "JOB",
|
|
207
|
+
"jobs": [
|
|
208
|
+
{
|
|
209
|
+
"functions": ["state.apply"],
|
|
210
|
+
"arguments": [["<properties.startTestStateId>"]],
|
|
211
|
+
"metadata": {},
|
|
212
|
+
}
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
"states": [
|
|
218
|
+
{
|
|
219
|
+
"name": "NEW",
|
|
220
|
+
"dashboardAvailable": False,
|
|
221
|
+
"defaultSubstate": "NEW",
|
|
222
|
+
"substates": [
|
|
223
|
+
{
|
|
224
|
+
"name": "NEW",
|
|
225
|
+
"displayText": "New",
|
|
226
|
+
"availableActions": [
|
|
227
|
+
{
|
|
228
|
+
"action": "PLAN_SCHEDULE",
|
|
229
|
+
"nextState": "SCHEDULED",
|
|
230
|
+
"nextSubstate": "SCHEDULED",
|
|
231
|
+
"showInUI": True,
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"name": "SCHEDULED",
|
|
239
|
+
"dashboardAvailable": True,
|
|
240
|
+
"defaultSubstate": "SCHEDULED",
|
|
241
|
+
"substates": [
|
|
242
|
+
{
|
|
243
|
+
"name": "SCHEDULED",
|
|
244
|
+
"displayText": "Scheduled",
|
|
245
|
+
"availableActions": [
|
|
246
|
+
{
|
|
247
|
+
"action": "START",
|
|
248
|
+
"nextState": "IN_PROGRESS",
|
|
249
|
+
"nextSubstate": "IN_PROGRESS",
|
|
250
|
+
"showInUI": True,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"action": "RUN_NOTEBOOK",
|
|
254
|
+
"nextState": "IN_PROGRESS",
|
|
255
|
+
"nextSubstate": "IN_PROGRESS",
|
|
256
|
+
"showInUI": True,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
}
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "IN_PROGRESS",
|
|
264
|
+
"dashboardAvailable": True,
|
|
265
|
+
"defaultSubstate": "IN_PROGRESS",
|
|
266
|
+
"substates": [
|
|
267
|
+
{
|
|
268
|
+
"name": "IN_PROGRESS",
|
|
269
|
+
"displayText": "In progress",
|
|
270
|
+
"availableActions": [
|
|
271
|
+
{
|
|
272
|
+
"action": "COMPLETE",
|
|
273
|
+
"nextState": "PENDING_APPROVAL",
|
|
274
|
+
"nextSubstate": "PENDING_APPROVAL",
|
|
275
|
+
"showInUI": True,
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"name": "PENDING_APPROVAL",
|
|
283
|
+
"dashboardAvailable": True,
|
|
284
|
+
"defaultSubstate": "PENDING_APPROVAL",
|
|
285
|
+
"substates": [
|
|
286
|
+
{
|
|
287
|
+
"name": "PENDING_APPROVAL",
|
|
288
|
+
"displayText": "Pending approval",
|
|
289
|
+
"availableActions": [
|
|
290
|
+
{
|
|
291
|
+
"action": "RUN_JOB",
|
|
292
|
+
"nextState": "CLOSED",
|
|
293
|
+
"nextSubstate": "CLOSED",
|
|
294
|
+
"showInUI": True,
|
|
295
|
+
}
|
|
296
|
+
],
|
|
297
|
+
}
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "CLOSED",
|
|
302
|
+
"dashboardAvailable": False,
|
|
303
|
+
"defaultSubstate": "CLOSED",
|
|
304
|
+
"substates": [
|
|
305
|
+
{"name": "CLOSED", "displayText": "Closed", "availableActions": []}
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
"name": "CANCELED",
|
|
310
|
+
"dashboardAvailable": False,
|
|
311
|
+
"defaultSubstate": "CANCELED",
|
|
312
|
+
"substates": [
|
|
313
|
+
{"name": "CANCELED", "displayText": "Canceled", "availableActions": []}
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
# Check if file already exists
|
|
321
|
+
if os.path.exists(output):
|
|
322
|
+
if not questionary.confirm(
|
|
323
|
+
f"File {output} already exists. Overwrite?",
|
|
324
|
+
default=False,
|
|
325
|
+
).ask():
|
|
326
|
+
click.echo("Workflow initialization cancelled.")
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
# Save the workflow file
|
|
330
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
331
|
+
json.dump(workflow_data, f, indent=2, ensure_ascii=False)
|
|
332
|
+
|
|
333
|
+
click.echo(f"✓ Workflow initialized: {output}")
|
|
334
|
+
click.echo("Edit the file to customize your workflow:")
|
|
335
|
+
click.echo(" - name and description are recommended")
|
|
336
|
+
click.echo(" - Define states, substates, and actions as needed")
|
|
337
|
+
click.echo(f" - Workspace is set to: {workspace} (ID: {workspace_id})")
|
|
338
|
+
click.echo(" - Use 'slcli workflows import' to upload the workflow when ready")
|
|
339
|
+
|
|
340
|
+
except Exception as exc:
|
|
341
|
+
click.echo(f"✗ Error creating workflow file: {exc}", err=True)
|
|
342
|
+
sys.exit(ExitCodes.GENERAL_ERROR)
|
|
343
|
+
|
|
344
|
+
@workflow.command(name="list")
|
|
345
|
+
@click.option(
|
|
346
|
+
"--workspace",
|
|
347
|
+
"-w",
|
|
348
|
+
help="Filter by workspace name or ID",
|
|
349
|
+
)
|
|
350
|
+
@click.option(
|
|
351
|
+
"--take",
|
|
352
|
+
"-t",
|
|
353
|
+
type=int,
|
|
354
|
+
default=25,
|
|
355
|
+
show_default=True,
|
|
356
|
+
help="Maximum number of workflows to return",
|
|
357
|
+
)
|
|
358
|
+
@click.option(
|
|
359
|
+
"--format",
|
|
360
|
+
"-f",
|
|
361
|
+
type=click.Choice(["table", "json"]),
|
|
362
|
+
default="table",
|
|
363
|
+
show_default=True,
|
|
364
|
+
help="Output format",
|
|
365
|
+
)
|
|
366
|
+
@click.option(
|
|
367
|
+
"--status",
|
|
368
|
+
"-s",
|
|
369
|
+
help="Filter by workflow status",
|
|
370
|
+
)
|
|
371
|
+
def list_workflows(
|
|
372
|
+
format: str = "table",
|
|
373
|
+
workspace: Optional[str] = None,
|
|
374
|
+
take: int = 25,
|
|
375
|
+
status: Optional[str] = None,
|
|
376
|
+
) -> None:
|
|
377
|
+
"""List available workflows."""
|
|
378
|
+
format_output = validate_output_format(format)
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
workspace_map = get_workspace_map()
|
|
382
|
+
|
|
383
|
+
# Resolve workspace filter to ID if specified
|
|
384
|
+
workspace_id = None
|
|
385
|
+
workspace = get_effective_workspace(workspace)
|
|
386
|
+
if workspace:
|
|
387
|
+
workspace_id = resolve_workspace_filter(workspace, workspace_map)
|
|
388
|
+
|
|
389
|
+
# Use continuation token pagination to get all workflows
|
|
390
|
+
all_workflows = _query_all_workflows(workspace_id, workspace_map)
|
|
391
|
+
|
|
392
|
+
# Create a mock response with all data
|
|
393
|
+
resp = FilteredResponse({"workflows": all_workflows})
|
|
394
|
+
|
|
395
|
+
# Use universal response handler with workflow formatter
|
|
396
|
+
def workflow_formatter(workflow: dict) -> list:
|
|
397
|
+
ws_guid = workflow.get("workspace", "")
|
|
398
|
+
ws_name = get_workspace_display_name(ws_guid, workspace_map)
|
|
399
|
+
return [
|
|
400
|
+
workflow.get("name", "Unknown"),
|
|
401
|
+
ws_name,
|
|
402
|
+
workflow.get("id", ""),
|
|
403
|
+
workflow.get("description", "N/A")[:30], # Truncate description
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
UniversalResponseHandler.handle_list_response(
|
|
407
|
+
resp=resp,
|
|
408
|
+
data_key="workflows",
|
|
409
|
+
item_name="workflow",
|
|
410
|
+
format_output=format_output,
|
|
411
|
+
formatter_func=workflow_formatter,
|
|
412
|
+
headers=["Name", "Workspace", "ID", "Description"],
|
|
413
|
+
column_widths=[40, 30, 36, 32],
|
|
414
|
+
empty_message="No workflows found.",
|
|
415
|
+
enable_pagination=True,
|
|
416
|
+
page_size=take,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
except Exception as exc:
|
|
420
|
+
handle_api_error(exc)
|
|
421
|
+
|
|
422
|
+
@workflow.command(name="get")
|
|
423
|
+
@click.option("--id", "-i", "workflow_id", help="Workflow ID")
|
|
424
|
+
@click.option("--name", "-n", "workflow_name", help="Workflow name")
|
|
425
|
+
@click.option(
|
|
426
|
+
"--format",
|
|
427
|
+
"-f",
|
|
428
|
+
type=click.Choice(["table", "json"]),
|
|
429
|
+
default="table",
|
|
430
|
+
show_default=True,
|
|
431
|
+
help="Output format",
|
|
432
|
+
)
|
|
433
|
+
def get_workflow(
|
|
434
|
+
workflow_id: Optional[str] = None,
|
|
435
|
+
workflow_name: Optional[str] = None,
|
|
436
|
+
format: str = "table",
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Show workflow details by ID or name."""
|
|
439
|
+
if not workflow_id and not workflow_name:
|
|
440
|
+
click.echo("✗ Must provide either --id or --name.", err=True)
|
|
441
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
442
|
+
if workflow_id and workflow_name:
|
|
443
|
+
click.echo("✗ Cannot specify both --id and --name.", err=True)
|
|
444
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
445
|
+
|
|
446
|
+
format_output = validate_output_format(format)
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
# If name is provided, find the workflow by name first
|
|
450
|
+
if workflow_name:
|
|
451
|
+
query_url = f"{get_base_url()}/niworkorder/v1/query-workflows?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
452
|
+
query_payload = {
|
|
453
|
+
"take": 1000,
|
|
454
|
+
"filter": f'NAME == "{workflow_name}"',
|
|
455
|
+
}
|
|
456
|
+
query_resp = make_api_request("POST", query_url, query_payload)
|
|
457
|
+
query_data = query_resp.json()
|
|
458
|
+
workflows = query_data.get("workflows", [])
|
|
459
|
+
|
|
460
|
+
# Find exact match
|
|
461
|
+
matching = [w for w in workflows if w.get("name") == workflow_name]
|
|
462
|
+
if not matching:
|
|
463
|
+
click.echo(f"✗ Workflow '{workflow_name}' not found.", err=True)
|
|
464
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
465
|
+
workflow_id = matching[0].get("id", "")
|
|
466
|
+
|
|
467
|
+
# Fetch the workflow by ID
|
|
468
|
+
url = f"{get_base_url()}/niworkorder/v1/workflows/{workflow_id}?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
469
|
+
resp = make_api_request("GET", url)
|
|
470
|
+
workflow = resp.json()
|
|
471
|
+
|
|
472
|
+
if not workflow:
|
|
473
|
+
identifier = workflow_id if not workflow_name else workflow_name
|
|
474
|
+
click.echo(f"✗ Workflow '{identifier}' not found.", err=True)
|
|
475
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
476
|
+
|
|
477
|
+
if format_output == "json":
|
|
478
|
+
click.echo(json.dumps(workflow, indent=2))
|
|
479
|
+
return
|
|
480
|
+
|
|
481
|
+
# Table format
|
|
482
|
+
workspace_map = get_workspace_map()
|
|
483
|
+
ws_name = get_workspace_display_name(
|
|
484
|
+
workflow.get("workspace", ""),
|
|
485
|
+
workspace_map,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
click.echo("Workflow Details:")
|
|
489
|
+
click.echo("=" * 50)
|
|
490
|
+
click.echo(f"Name: {workflow.get('name', 'N/A')}")
|
|
491
|
+
click.echo(f"ID: {workflow.get('id', 'N/A')}")
|
|
492
|
+
click.echo(f"Workspace: {ws_name}")
|
|
493
|
+
click.echo(f"Description: {workflow.get('description', 'N/A')}")
|
|
494
|
+
click.echo(f"State: {workflow.get('state', 'N/A')}")
|
|
495
|
+
|
|
496
|
+
except Exception as exc:
|
|
497
|
+
handle_api_error(exc)
|
|
498
|
+
|
|
499
|
+
@workflow.command(name="export")
|
|
500
|
+
@click.option("--id", "-i", "workflow_id", help="Workflow ID to export")
|
|
501
|
+
@click.option("--name", "-n", "workflow_name", help="Workflow name to export")
|
|
502
|
+
@click.option("--output", "-o", help="Output JSON file (default: <workflow-name>.json)")
|
|
503
|
+
def export_workflow(
|
|
504
|
+
workflow_id: Optional[str] = None,
|
|
505
|
+
workflow_name: Optional[str] = None,
|
|
506
|
+
output: Optional[str] = None,
|
|
507
|
+
) -> None:
|
|
508
|
+
"""Export a workflow to JSON by ID or name."""
|
|
509
|
+
if not workflow_id and not workflow_name:
|
|
510
|
+
click.echo("✗ Must provide either --id or --name.", err=True)
|
|
511
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
512
|
+
if workflow_id and workflow_name:
|
|
513
|
+
click.echo("✗ Cannot specify both --id and --name.", err=True)
|
|
514
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
515
|
+
|
|
516
|
+
# If name is provided, find the workflow by name first
|
|
517
|
+
if workflow_name:
|
|
518
|
+
query_url = f"{get_base_url()}/niworkorder/v1/query-workflows?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
519
|
+
query_payload = {
|
|
520
|
+
"take": 1000,
|
|
521
|
+
"filter": f'NAME == "{workflow_name}"',
|
|
522
|
+
}
|
|
523
|
+
query_resp = make_api_request("POST", query_url, query_payload)
|
|
524
|
+
query_data = query_resp.json()
|
|
525
|
+
workflows = query_data.get("workflows", [])
|
|
526
|
+
|
|
527
|
+
# Find exact match (filter is case-insensitive substring)
|
|
528
|
+
matching = [w for w in workflows if w.get("name") == workflow_name]
|
|
529
|
+
if not matching:
|
|
530
|
+
click.echo(f"✗ Workflow '{workflow_name}' not found.", err=True)
|
|
531
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
532
|
+
workflow_id = matching[0].get("id", "")
|
|
533
|
+
|
|
534
|
+
# Fetch the workflow by ID
|
|
535
|
+
url = f"{get_base_url()}/niworkorder/v1/workflows/{workflow_id}?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
536
|
+
try:
|
|
537
|
+
resp = make_api_request("GET", url)
|
|
538
|
+
data = resp.json()
|
|
539
|
+
|
|
540
|
+
if not data:
|
|
541
|
+
identifier = workflow_id if not workflow_name else workflow_name
|
|
542
|
+
click.echo(f"✗ Workflow '{identifier}' not found.", err=True)
|
|
543
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
544
|
+
|
|
545
|
+
# Generate output filename if not provided
|
|
546
|
+
if not output:
|
|
547
|
+
workflow_name = data.get("name", f"workflow-{workflow_id}")
|
|
548
|
+
safe_name = sanitize_filename(workflow_name, f"workflow-{workflow_id}")
|
|
549
|
+
output = f"{safe_name}.json"
|
|
550
|
+
|
|
551
|
+
save_json_file(data, output)
|
|
552
|
+
click.echo(f"✓ Workflow exported to {output}")
|
|
553
|
+
except Exception as exc:
|
|
554
|
+
if "not found" not in str(exc).lower():
|
|
555
|
+
handle_api_error(exc)
|
|
556
|
+
else:
|
|
557
|
+
click.echo(f"✗ Error: {exc}", err=True)
|
|
558
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
559
|
+
|
|
560
|
+
@workflow.command(name="import")
|
|
561
|
+
@click.option(
|
|
562
|
+
"--file",
|
|
563
|
+
"input_file",
|
|
564
|
+
required=True,
|
|
565
|
+
help="Input JSON file",
|
|
566
|
+
)
|
|
567
|
+
@click.option(
|
|
568
|
+
"--workspace",
|
|
569
|
+
"-w",
|
|
570
|
+
help="Override workspace name or ID (uses value from file if not specified)",
|
|
571
|
+
)
|
|
572
|
+
def import_workflow(input_file: str, workspace: Optional[str]) -> None:
|
|
573
|
+
"""Import a workflow from JSON.
|
|
574
|
+
|
|
575
|
+
Workspace can be specified via --workspace flag or included in the JSON file.
|
|
576
|
+
Command line workspace takes precedence over file contents.
|
|
577
|
+
"""
|
|
578
|
+
from .utils import check_readonly_mode
|
|
579
|
+
|
|
580
|
+
check_readonly_mode("import a workflow")
|
|
581
|
+
|
|
582
|
+
url = f"{get_base_url()}/niworkorder/v1/workflows?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
583
|
+
allowed_fields = {
|
|
584
|
+
"name",
|
|
585
|
+
"description",
|
|
586
|
+
"actions",
|
|
587
|
+
"states",
|
|
588
|
+
"workspace",
|
|
589
|
+
}
|
|
590
|
+
try:
|
|
591
|
+
data = load_json_file(input_file)
|
|
592
|
+
|
|
593
|
+
# Filter allowed fields
|
|
594
|
+
filtered_data = {k: v for k, v in data.items() if k in allowed_fields}
|
|
595
|
+
|
|
596
|
+
# Handle workspace resolution
|
|
597
|
+
if workspace:
|
|
598
|
+
# Override workspace from command line
|
|
599
|
+
try:
|
|
600
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
601
|
+
filtered_data["workspace"] = workspace_id
|
|
602
|
+
except Exception as exc:
|
|
603
|
+
click.echo(f"✗ Error resolving workspace '{workspace}': {exc}", err=True)
|
|
604
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
605
|
+
elif "workspace" not in filtered_data or not filtered_data["workspace"]:
|
|
606
|
+
# No workspace specified and none in file - try profile default
|
|
607
|
+
workspace = get_effective_workspace(None)
|
|
608
|
+
if workspace:
|
|
609
|
+
try:
|
|
610
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
611
|
+
filtered_data["workspace"] = workspace_id
|
|
612
|
+
except Exception as exc:
|
|
613
|
+
click.echo(f"✗ Error resolving workspace '{workspace}': {exc}", err=True)
|
|
614
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
615
|
+
else:
|
|
616
|
+
click.echo(
|
|
617
|
+
"✗ Workspace is required. Specify --workspace or include 'workspace' in the JSON file.",
|
|
618
|
+
err=True,
|
|
619
|
+
)
|
|
620
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
621
|
+
elif filtered_data["workspace"] and not filtered_data["workspace"].startswith("//"):
|
|
622
|
+
# Workspace in file - validate/resolve it if it looks like a name
|
|
623
|
+
try:
|
|
624
|
+
workspace_id = get_workspace_id_with_fallback(filtered_data["workspace"])
|
|
625
|
+
filtered_data["workspace"] = workspace_id
|
|
626
|
+
except Exception as exc:
|
|
627
|
+
click.echo(
|
|
628
|
+
f"✗ Error resolving workspace from file '{filtered_data['workspace']}': {exc}",
|
|
629
|
+
err=True,
|
|
630
|
+
)
|
|
631
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
resp = make_api_request("POST", url, filtered_data, handle_errors=False)
|
|
635
|
+
# Check for successful creation
|
|
636
|
+
if resp.status_code == 201:
|
|
637
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
638
|
+
workflow_id = response_data.get("id", "")
|
|
639
|
+
if workflow_id:
|
|
640
|
+
click.echo(f"✓ Workflow imported successfully with ID: {workflow_id}")
|
|
641
|
+
else:
|
|
642
|
+
click.echo("✓ Workflow imported successfully.")
|
|
643
|
+
else:
|
|
644
|
+
# Handle error responses - parse detailed error structure
|
|
645
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
646
|
+
_handle_workflow_error_response(response_data, "Workflow import failed")
|
|
647
|
+
except requests.exceptions.HTTPError as http_exc:
|
|
648
|
+
# Extract response data from the HTTP error for detailed parsing
|
|
649
|
+
if hasattr(http_exc, "response") and http_exc.response is not None:
|
|
650
|
+
try:
|
|
651
|
+
response_data = (
|
|
652
|
+
http_exc.response.json() if http_exc.response.text.strip() else {}
|
|
653
|
+
)
|
|
654
|
+
_handle_workflow_error_response(response_data, "Workflow import failed")
|
|
655
|
+
except Exception:
|
|
656
|
+
# Fallback to generic error handling if JSON parsing fails
|
|
657
|
+
handle_api_error(http_exc)
|
|
658
|
+
else:
|
|
659
|
+
handle_api_error(http_exc)
|
|
660
|
+
|
|
661
|
+
except Exception as exc:
|
|
662
|
+
handle_api_error(exc)
|
|
663
|
+
|
|
664
|
+
@workflow.command(name="delete")
|
|
665
|
+
@click.option(
|
|
666
|
+
"--id",
|
|
667
|
+
"-i",
|
|
668
|
+
"workflow_id",
|
|
669
|
+
required=True,
|
|
670
|
+
help="Workflow ID to delete",
|
|
671
|
+
)
|
|
672
|
+
@click.confirmation_option(prompt="Are you sure you want to delete this workflow?")
|
|
673
|
+
def delete_workflow(workflow_id: str) -> None:
|
|
674
|
+
"""Delete a workflow."""
|
|
675
|
+
from .utils import check_readonly_mode
|
|
676
|
+
|
|
677
|
+
check_readonly_mode("delete a workflow")
|
|
678
|
+
|
|
679
|
+
url = f"{get_base_url()}/niworkorder/v1/delete-workflows?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
680
|
+
payload = {"ids": [workflow_id]}
|
|
681
|
+
try:
|
|
682
|
+
try:
|
|
683
|
+
resp = make_api_request("POST", url, payload, handle_errors=False)
|
|
684
|
+
if resp.status_code in (200, 204):
|
|
685
|
+
# Parse the response to check for partial failures
|
|
686
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
687
|
+
_handle_workflow_delete_response(response_data, workflow_id)
|
|
688
|
+
else:
|
|
689
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
690
|
+
_handle_workflow_delete_response(response_data, workflow_id)
|
|
691
|
+
except requests.exceptions.HTTPError as http_exc:
|
|
692
|
+
# Extract response data from the HTTP error for detailed parsing
|
|
693
|
+
if hasattr(http_exc, "response") and http_exc.response is not None:
|
|
694
|
+
try:
|
|
695
|
+
response_data = (
|
|
696
|
+
http_exc.response.json() if http_exc.response.text.strip() else {}
|
|
697
|
+
)
|
|
698
|
+
_handle_workflow_delete_response(response_data, workflow_id)
|
|
699
|
+
except Exception:
|
|
700
|
+
# Fallback to generic error handling if JSON parsing fails
|
|
701
|
+
handle_api_error(http_exc)
|
|
702
|
+
else:
|
|
703
|
+
handle_api_error(http_exc)
|
|
704
|
+
except Exception as exc:
|
|
705
|
+
handle_api_error(exc)
|
|
706
|
+
|
|
707
|
+
@workflow.command(name="update")
|
|
708
|
+
@click.option(
|
|
709
|
+
"--id",
|
|
710
|
+
"-i",
|
|
711
|
+
"workflow_id",
|
|
712
|
+
required=True,
|
|
713
|
+
help="Workflow ID to update",
|
|
714
|
+
)
|
|
715
|
+
@click.option(
|
|
716
|
+
"--file",
|
|
717
|
+
"-f",
|
|
718
|
+
"input_file",
|
|
719
|
+
required=True,
|
|
720
|
+
help="Input JSON file with updated workflow data",
|
|
721
|
+
)
|
|
722
|
+
@click.option(
|
|
723
|
+
"--workspace",
|
|
724
|
+
"-w",
|
|
725
|
+
help="Override workspace name or ID (uses value from file if not specified)",
|
|
726
|
+
)
|
|
727
|
+
def update_workflow(workflow_id: str, input_file: str, workspace: Optional[str]) -> None:
|
|
728
|
+
"""Update a workflow from JSON.
|
|
729
|
+
|
|
730
|
+
Workspace can be specified via --workspace flag or included in the JSON file.
|
|
731
|
+
Command line workspace takes precedence over file contents.
|
|
732
|
+
"""
|
|
733
|
+
from .utils import check_readonly_mode
|
|
734
|
+
|
|
735
|
+
check_readonly_mode("update a workflow")
|
|
736
|
+
|
|
737
|
+
url = f"{get_base_url()}/niworkorder/v1/workflows/{workflow_id}?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
738
|
+
allowed_fields = {
|
|
739
|
+
"name",
|
|
740
|
+
"description",
|
|
741
|
+
"actions",
|
|
742
|
+
"states",
|
|
743
|
+
"workspace",
|
|
744
|
+
}
|
|
745
|
+
try:
|
|
746
|
+
data = load_json_file(input_file)
|
|
747
|
+
|
|
748
|
+
# Filter allowed fields
|
|
749
|
+
filtered_data = {k: v for k, v in data.items() if k in allowed_fields}
|
|
750
|
+
|
|
751
|
+
# Handle workspace resolution
|
|
752
|
+
if workspace:
|
|
753
|
+
# Override workspace from command line
|
|
754
|
+
try:
|
|
755
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
756
|
+
filtered_data["workspace"] = workspace_id
|
|
757
|
+
except Exception as exc:
|
|
758
|
+
click.echo(f"✗ Error resolving workspace '{workspace}': {exc}", err=True)
|
|
759
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
760
|
+
elif "workspace" not in filtered_data or not filtered_data.get("workspace"):
|
|
761
|
+
# No workspace specified and none in file - try profile default
|
|
762
|
+
workspace = get_effective_workspace(None)
|
|
763
|
+
if workspace:
|
|
764
|
+
try:
|
|
765
|
+
workspace_id = get_workspace_id_with_fallback(workspace)
|
|
766
|
+
filtered_data["workspace"] = workspace_id
|
|
767
|
+
except Exception as exc:
|
|
768
|
+
click.echo(f"✗ Error resolving workspace '{workspace}': {exc}", err=True)
|
|
769
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
770
|
+
elif (
|
|
771
|
+
"workspace" in filtered_data
|
|
772
|
+
and filtered_data["workspace"]
|
|
773
|
+
and not filtered_data["workspace"].startswith("//")
|
|
774
|
+
):
|
|
775
|
+
# Workspace in file - validate/resolve it if it looks like a name
|
|
776
|
+
try:
|
|
777
|
+
workspace_id = get_workspace_id_with_fallback(filtered_data["workspace"])
|
|
778
|
+
filtered_data["workspace"] = workspace_id
|
|
779
|
+
except Exception as exc:
|
|
780
|
+
click.echo(
|
|
781
|
+
f"✗ Error resolving workspace from file '{filtered_data['workspace']}': {exc}",
|
|
782
|
+
err=True,
|
|
783
|
+
)
|
|
784
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
785
|
+
|
|
786
|
+
try:
|
|
787
|
+
resp = make_api_request("PUT", url, filtered_data, handle_errors=False)
|
|
788
|
+
# Check for successful update
|
|
789
|
+
if resp.status_code == 200:
|
|
790
|
+
click.echo(f"✓ Workflow {workflow_id} updated successfully.")
|
|
791
|
+
else:
|
|
792
|
+
# Handle error responses
|
|
793
|
+
response_data = resp.json() if resp.text.strip() else {}
|
|
794
|
+
_handle_workflow_error_response(response_data, "Workflow update failed")
|
|
795
|
+
except requests.exceptions.HTTPError as http_exc:
|
|
796
|
+
# Extract response data from the HTTP error for detailed parsing
|
|
797
|
+
if hasattr(http_exc, "response") and http_exc.response is not None:
|
|
798
|
+
try:
|
|
799
|
+
response_data = (
|
|
800
|
+
http_exc.response.json() if http_exc.response.text.strip() else {}
|
|
801
|
+
)
|
|
802
|
+
_handle_workflow_error_response(response_data, "Workflow update failed")
|
|
803
|
+
except Exception:
|
|
804
|
+
# Fallback to generic error handling if JSON parsing fails
|
|
805
|
+
handle_api_error(http_exc)
|
|
806
|
+
else:
|
|
807
|
+
handle_api_error(http_exc)
|
|
808
|
+
|
|
809
|
+
except Exception as exc:
|
|
810
|
+
handle_api_error(exc)
|
|
811
|
+
|
|
812
|
+
@workflow.command(name="preview")
|
|
813
|
+
@click.option("--id", "-i", "workflow_id", help="Workflow ID to preview")
|
|
814
|
+
@click.option(
|
|
815
|
+
"--file",
|
|
816
|
+
"-f",
|
|
817
|
+
"input_file",
|
|
818
|
+
help="Local JSON file to preview (use '-' for stdin)",
|
|
819
|
+
)
|
|
820
|
+
@click.option("--output", "-o", help="Output file path (default: open in browser)")
|
|
821
|
+
@click.option(
|
|
822
|
+
"--format",
|
|
823
|
+
type=click.Choice(["html", "mmd"]),
|
|
824
|
+
default="html",
|
|
825
|
+
show_default=True,
|
|
826
|
+
help="Output format",
|
|
827
|
+
)
|
|
828
|
+
@click.option("--no-emoji", is_flag=True, default=False, help="Disable emoji in action labels")
|
|
829
|
+
@click.option(
|
|
830
|
+
"--no-legend",
|
|
831
|
+
is_flag=True,
|
|
832
|
+
default=False,
|
|
833
|
+
help="Disable legend block in HTML output",
|
|
834
|
+
)
|
|
835
|
+
@click.option(
|
|
836
|
+
"--no-open",
|
|
837
|
+
is_flag=True,
|
|
838
|
+
default=False,
|
|
839
|
+
help="Do not auto-open browser for HTML when no --output is provided",
|
|
840
|
+
)
|
|
841
|
+
def preview_workflow(
|
|
842
|
+
workflow_id: Optional[str],
|
|
843
|
+
input_file: Optional[str],
|
|
844
|
+
output: Optional[str],
|
|
845
|
+
format: str,
|
|
846
|
+
no_emoji: bool,
|
|
847
|
+
no_legend: bool,
|
|
848
|
+
no_open: bool,
|
|
849
|
+
) -> None:
|
|
850
|
+
"""Preview a workflow file without applying changes."""
|
|
851
|
+
from .utils import ExitCodes
|
|
852
|
+
|
|
853
|
+
if bool(workflow_id) == bool(input_file):
|
|
854
|
+
click.echo(
|
|
855
|
+
"✗ Must specify exactly one of --id or --file (use --file - for stdin)",
|
|
856
|
+
err=True,
|
|
857
|
+
)
|
|
858
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
859
|
+
try:
|
|
860
|
+
if workflow_id:
|
|
861
|
+
url = f"{get_base_url()}/niworkorder/v1/workflows/{workflow_id}?ff-userdefinedworkflowsfortestplaninstances=true"
|
|
862
|
+
resp = make_api_request("GET", url)
|
|
863
|
+
workflow_data: Dict[str, Any] = resp.json()
|
|
864
|
+
if not workflow_data:
|
|
865
|
+
click.echo(f"✗ Workflow with ID {workflow_id} not found.", err=True)
|
|
866
|
+
sys.exit(ExitCodes.NOT_FOUND)
|
|
867
|
+
else:
|
|
868
|
+
if input_file == "-":
|
|
869
|
+
try:
|
|
870
|
+
raw = sys.stdin.read()
|
|
871
|
+
workflow_data = json.loads(raw)
|
|
872
|
+
except json.JSONDecodeError as exc:
|
|
873
|
+
click.echo(f"✗ Invalid JSON from stdin: {exc}", err=True)
|
|
874
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
875
|
+
else:
|
|
876
|
+
assert input_file is not None
|
|
877
|
+
workflow_data = load_json_file(input_file)
|
|
878
|
+
mermaid_code = workflow_preview.generate_mermaid_diagram(
|
|
879
|
+
workflow_data, enable_emoji=not no_emoji
|
|
880
|
+
)
|
|
881
|
+
if format == "mmd":
|
|
882
|
+
if not output:
|
|
883
|
+
output = f"workflow-{workflow_data.get('name', 'preview')}.mmd"
|
|
884
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
885
|
+
f.write(mermaid_code)
|
|
886
|
+
click.echo(f"✓ Mermaid diagram saved to {output}")
|
|
887
|
+
else:
|
|
888
|
+
html_content = workflow_preview.generate_html_with_mermaid(
|
|
889
|
+
workflow_data, mermaid_code, include_legend=not no_legend
|
|
890
|
+
)
|
|
891
|
+
if output:
|
|
892
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
893
|
+
f.write(html_content)
|
|
894
|
+
click.echo(f"✓ HTML preview saved to {output}")
|
|
895
|
+
elif not no_open:
|
|
896
|
+
with tempfile.NamedTemporaryFile(
|
|
897
|
+
mode="w", suffix=".html", delete=False, encoding="utf-8"
|
|
898
|
+
) as f:
|
|
899
|
+
f.write(html_content)
|
|
900
|
+
temp_file = f.name
|
|
901
|
+
webbrowser.open(f"file://{Path(temp_file).absolute()}")
|
|
902
|
+
click.echo("✓ Opening workflow preview in browser...")
|
|
903
|
+
else:
|
|
904
|
+
click.echo(html_content)
|
|
905
|
+
except Exception as exc: # noqa: BLE001
|
|
906
|
+
handle_api_error(exc)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def _handle_workflow_error_response(response_data: Dict[str, Any], operation_name: str) -> None:
|
|
910
|
+
"""Parse and display detailed workflow error responses.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
response_data: The JSON response data containing error information
|
|
914
|
+
operation_name: The name of the operation that failed (e.g., "Workflow import failed")
|
|
915
|
+
"""
|
|
916
|
+
display_api_errors(operation_name, response_data, detailed=True)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _handle_workflow_delete_response(response_data: Dict[str, Any], workflow_id: str) -> None:
|
|
920
|
+
"""Parse and display workflow delete response, handling both success and failures.
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
response_data: The JSON response data from delete operation
|
|
924
|
+
workflow_id: The ID of the workflow that was requested to be deleted
|
|
925
|
+
"""
|
|
926
|
+
# Handle successful deletion response: empty JSON {} with 204 status
|
|
927
|
+
if not response_data or response_data == {}:
|
|
928
|
+
click.echo(f"✓ Workflow {workflow_id} deleted successfully.")
|
|
929
|
+
return
|
|
930
|
+
|
|
931
|
+
# Handle successful deletion response format: {"ids": ["1023"]}
|
|
932
|
+
if "ids" in response_data:
|
|
933
|
+
deleted_ids = response_data.get("ids", [])
|
|
934
|
+
if workflow_id in deleted_ids:
|
|
935
|
+
click.echo(f"✓ Workflow {workflow_id} deleted successfully.")
|
|
936
|
+
return
|
|
937
|
+
else:
|
|
938
|
+
# Workflow ID not in the successful deletion list - unexpected
|
|
939
|
+
click.echo(f"✗ Unexpected response for workflow {workflow_id}:", err=True)
|
|
940
|
+
click.echo(f" Successfully deleted: {', '.join(deleted_ids)}", err=True)
|
|
941
|
+
sys.exit(1)
|
|
942
|
+
|
|
943
|
+
# Handle error response format with deletedWorkflowIds and failedWorkflowIds
|
|
944
|
+
deleted_ids = response_data.get("deletedWorkflowIds", [])
|
|
945
|
+
failed_ids = response_data.get("failedWorkflowIds", [])
|
|
946
|
+
|
|
947
|
+
# Check if our specific workflow was deleted successfully
|
|
948
|
+
if workflow_id in deleted_ids:
|
|
949
|
+
click.echo(f"✓ Workflow {workflow_id} deleted successfully.")
|
|
950
|
+
return
|
|
951
|
+
|
|
952
|
+
# Check if our specific workflow failed to delete
|
|
953
|
+
if workflow_id in failed_ids:
|
|
954
|
+
click.echo(f"✗ Failed to delete workflow {workflow_id}:", err=True)
|
|
955
|
+
|
|
956
|
+
# Parse error details for failed workflows
|
|
957
|
+
error = response_data.get("error", {})
|
|
958
|
+
if error:
|
|
959
|
+
main_message = error.get("message", "Unknown error")
|
|
960
|
+
click.echo(f" {main_message}", err=True)
|
|
961
|
+
|
|
962
|
+
# Parse inner errors for detailed failure reasons
|
|
963
|
+
inner_errors = error.get("innerErrors", [])
|
|
964
|
+
for inner_error in inner_errors:
|
|
965
|
+
resource_id = inner_error.get("resourceId", "")
|
|
966
|
+
if resource_id == workflow_id:
|
|
967
|
+
error_message = inner_error.get("message", "Unknown error")
|
|
968
|
+
error_name = inner_error.get("name", "")
|
|
969
|
+
|
|
970
|
+
# Extract more readable error type
|
|
971
|
+
if error_name:
|
|
972
|
+
error_type = extract_error_type(error_name)
|
|
973
|
+
click.echo(f" - {error_type}: {error_message}", err=True)
|
|
974
|
+
else:
|
|
975
|
+
click.echo(f" - {error_message}", err=True)
|
|
976
|
+
|
|
977
|
+
sys.exit(1)
|
|
978
|
+
|
|
979
|
+
# If workflow ID is not in either list, something unexpected happened
|
|
980
|
+
click.echo(f"✗ Unexpected response for workflow {workflow_id}:", err=True)
|
|
981
|
+
if deleted_ids:
|
|
982
|
+
click.echo(f" Successfully deleted: {', '.join(deleted_ids)}", err=True)
|
|
983
|
+
if failed_ids:
|
|
984
|
+
click.echo(f" Failed to delete: {', '.join(failed_ids)}", err=True)
|
|
985
|
+
|
|
986
|
+
# If there were any failures or unexpected responses, exit with error code
|
|
987
|
+
if failed_ids or not response_data:
|
|
988
|
+
sys.exit(1)
|