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
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Workflow preview (Mermaid diagram) generation utilities.
|
|
2
|
+
|
|
3
|
+
Separated from workflows_click.py to keep command module concise.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, List, Tuple, Optional
|
|
9
|
+
|
|
10
|
+
# Public-ish constants (imported by tests / commands)
|
|
11
|
+
ACTION_TYPE_EMOJI: Dict[str, str] = {
|
|
12
|
+
"MANUAL": "🧑",
|
|
13
|
+
"NOTEBOOK": "📓",
|
|
14
|
+
"SCHEDULE": "📅",
|
|
15
|
+
"JOB": "🛠️",
|
|
16
|
+
}
|
|
17
|
+
LEGEND_ROWS: List[Tuple[str, str]] = [
|
|
18
|
+
("🧑", "Manual action"),
|
|
19
|
+
("📓", "Notebook action"),
|
|
20
|
+
("📅", "Schedule action"),
|
|
21
|
+
("🛠️", "Job action"),
|
|
22
|
+
("(priv1, priv2)", "Privileges required"),
|
|
23
|
+
("NB abcdef..", "Truncated Notebook ID"),
|
|
24
|
+
("⚡️ NAME", "UI icon class"),
|
|
25
|
+
("hidden", "Hidden (not shown in UI)"),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sanitize_mermaid_label(label: str) -> str:
|
|
30
|
+
"""Sanitize label text for Mermaid diagram compatibility.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
label: Raw label text
|
|
34
|
+
Returns:
|
|
35
|
+
Sanitized label safe for inclusion inside Mermaid stateDiagram labels
|
|
36
|
+
"""
|
|
37
|
+
if not label:
|
|
38
|
+
return label
|
|
39
|
+
sanitized = label.replace("[", "(").replace("]", ")").replace("🔗", "")
|
|
40
|
+
# Keep backslashes (needed for Mermaid newline escapes) but normalize forward slashes
|
|
41
|
+
sanitized = sanitized.replace("/", "-")
|
|
42
|
+
sanitized = sanitized.replace('"', "'").replace("`", "'")
|
|
43
|
+
sanitized = sanitized.replace(":", " ")
|
|
44
|
+
sanitized = sanitized.replace(";", ",")
|
|
45
|
+
sanitized = sanitized.replace("|", " ")
|
|
46
|
+
sanitized = sanitized.replace("&", "and")
|
|
47
|
+
sanitized = " ".join(sanitized.split())
|
|
48
|
+
return sanitized
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def generate_mermaid_diagram(workflow_data: Dict[str, Any], enable_emoji: bool = True) -> str:
|
|
52
|
+
"""Generate Mermaid stateDiagram-v2 source for a workflow.
|
|
53
|
+
|
|
54
|
+
Now produces hierarchical composite states instead of flattening substates.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
workflow_data: Parsed workflow JSON dict
|
|
58
|
+
enable_emoji: Whether to include emoji for action types
|
|
59
|
+
"""
|
|
60
|
+
states: List[Dict[str, Any]] = workflow_data.get("states", [])
|
|
61
|
+
actions: List[Dict[str, Any]] = workflow_data.get("actions", [])
|
|
62
|
+
type_emojis = ACTION_TYPE_EMOJI if enable_emoji else {}
|
|
63
|
+
|
|
64
|
+
# Build action lookup for transition label enrichment
|
|
65
|
+
action_lookup: Dict[str, Dict[str, Any]] = {}
|
|
66
|
+
for action in actions:
|
|
67
|
+
name = action.get("name", "")
|
|
68
|
+
execution_action = action.get("executionAction", {})
|
|
69
|
+
info = {
|
|
70
|
+
"display": action.get("displayText", name),
|
|
71
|
+
"type": execution_action.get("type", ""),
|
|
72
|
+
"privileges": action.get("privilegeSpecificity", []),
|
|
73
|
+
"icon": action.get("iconClass"),
|
|
74
|
+
"notebook_id": execution_action.get("notebookId"),
|
|
75
|
+
}
|
|
76
|
+
action_lookup[name] = info
|
|
77
|
+
action_lookup[name.strip()] = info
|
|
78
|
+
|
|
79
|
+
lines: List[str] = ["stateDiagram-v2", ""]
|
|
80
|
+
|
|
81
|
+
# Mapping (state, substate) -> node identifier used in transitions
|
|
82
|
+
node_id_map: Dict[Tuple[str, str], str] = {}
|
|
83
|
+
|
|
84
|
+
def make_node_id(parent: str, sub: str) -> str:
|
|
85
|
+
# Ensure internal substate id never collides with the composite container name
|
|
86
|
+
if sub == parent:
|
|
87
|
+
base = f"{parent}_BASE"
|
|
88
|
+
else:
|
|
89
|
+
base = f"{parent}_{sub}"
|
|
90
|
+
# Sanitize ID: replace spaces and disallowed chars with underscores
|
|
91
|
+
safe = "".join(c if c.isalnum() or c in ("_",) else "_" for c in base)
|
|
92
|
+
return safe
|
|
93
|
+
|
|
94
|
+
# First pass: declare states (simple or composite) and internal nodes
|
|
95
|
+
for state in states:
|
|
96
|
+
raw_state_name = state.get("name")
|
|
97
|
+
if not isinstance(raw_state_name, str) or not raw_state_name:
|
|
98
|
+
# Skip invalid state entries
|
|
99
|
+
continue
|
|
100
|
+
state_name: str = raw_state_name
|
|
101
|
+
substates: List[Dict[str, Any]] = state.get("substates", [])
|
|
102
|
+
if not substates:
|
|
103
|
+
# No substates: simple state
|
|
104
|
+
display_label = state_name.replace("_", " ").title()
|
|
105
|
+
lines.append(f" {state_name}: {sanitize_mermaid_label(display_label)}")
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Determine if this should be composite: more than one substate OR differing names
|
|
109
|
+
unique_sub_names = {s.get("name") for s in substates if isinstance(s.get("name"), str)}
|
|
110
|
+
composite = len(unique_sub_names) > 1 or (
|
|
111
|
+
len(unique_sub_names) == 1 and state_name not in unique_sub_names
|
|
112
|
+
)
|
|
113
|
+
ds_val = state.get("defaultSubstate")
|
|
114
|
+
default_sub: Optional[str] = ds_val if isinstance(ds_val, str) else None
|
|
115
|
+
if composite:
|
|
116
|
+
lines.append(f" state {state_name} {{")
|
|
117
|
+
# Add initial pointer if default specified
|
|
118
|
+
if default_sub and any(s.get("name") == default_sub for s in substates):
|
|
119
|
+
default_id = make_node_id(state_name, default_sub)
|
|
120
|
+
lines.append(f" [*] --> {default_id}")
|
|
121
|
+
# Emit substate nodes
|
|
122
|
+
for sub in substates:
|
|
123
|
+
raw_sub_name = sub.get("name")
|
|
124
|
+
if not isinstance(raw_sub_name, str) or not raw_sub_name:
|
|
125
|
+
continue
|
|
126
|
+
sub_name: str = raw_sub_name
|
|
127
|
+
node_id = make_node_id(state_name, sub_name)
|
|
128
|
+
node_id_map[(state_name, sub_name)] = node_id
|
|
129
|
+
display = sub.get("displayText") or (
|
|
130
|
+
sub_name.replace("_", " ") if sub_name else state_name
|
|
131
|
+
)
|
|
132
|
+
metadata_parts: List[str] = []
|
|
133
|
+
if state.get("dashboardAvailable"):
|
|
134
|
+
metadata_parts.append("Dashboard")
|
|
135
|
+
available_actions = sub.get("availableActions", [])
|
|
136
|
+
visible_actions = len([a for a in available_actions if a.get("showInUI", True)])
|
|
137
|
+
hidden_actions = len([a for a in available_actions if not a.get("showInUI", True)])
|
|
138
|
+
if available_actions:
|
|
139
|
+
if hidden_actions:
|
|
140
|
+
metadata_parts.append(f"{visible_actions}+{hidden_actions} actions")
|
|
141
|
+
else:
|
|
142
|
+
metadata_parts.append(
|
|
143
|
+
f"{visible_actions} action{'s' if visible_actions != 1 else ''}"
|
|
144
|
+
)
|
|
145
|
+
if metadata_parts:
|
|
146
|
+
label_main = sanitize_mermaid_label(display)
|
|
147
|
+
label_meta = sanitize_mermaid_label(f"({', '.join(metadata_parts)})")
|
|
148
|
+
label = f"{label_main}\\n{label_meta}"
|
|
149
|
+
else:
|
|
150
|
+
label = sanitize_mermaid_label(display)
|
|
151
|
+
indent = " " if composite else " "
|
|
152
|
+
lines.append(f"{indent}{node_id}: {sanitize_mermaid_label(label)}")
|
|
153
|
+
if composite:
|
|
154
|
+
lines.append(" }")
|
|
155
|
+
|
|
156
|
+
# Second pass: transitions using node_id_map
|
|
157
|
+
for state in states:
|
|
158
|
+
raw_state_name = state.get("name")
|
|
159
|
+
if not isinstance(raw_state_name, str) or not raw_state_name:
|
|
160
|
+
continue
|
|
161
|
+
state_name = raw_state_name
|
|
162
|
+
for sub in state.get("substates", []):
|
|
163
|
+
source_sub = sub.get("name")
|
|
164
|
+
if not isinstance(source_sub, str) or not source_sub:
|
|
165
|
+
continue
|
|
166
|
+
source_id = node_id_map.get((state_name, source_sub)) or state_name
|
|
167
|
+
for a in sub.get("availableActions", []):
|
|
168
|
+
next_state = a.get("nextState")
|
|
169
|
+
if not next_state:
|
|
170
|
+
continue
|
|
171
|
+
next_sub = a.get("nextSubstate") or next_state
|
|
172
|
+
if not isinstance(next_state, str) or not isinstance(next_sub, str):
|
|
173
|
+
continue
|
|
174
|
+
target_id = (
|
|
175
|
+
node_id_map.get((next_state, next_sub))
|
|
176
|
+
or node_id_map.get((next_state, next_state))
|
|
177
|
+
or next_state
|
|
178
|
+
)
|
|
179
|
+
action_name = a.get("action", "")
|
|
180
|
+
show_in_ui = a.get("showInUI", True)
|
|
181
|
+
info = (
|
|
182
|
+
action_lookup.get(action_name) or action_lookup.get(action_name.strip()) or {}
|
|
183
|
+
)
|
|
184
|
+
emoji = type_emojis.get(info.get("type", ""), "")
|
|
185
|
+
segs: List[str] = []
|
|
186
|
+
if emoji:
|
|
187
|
+
segs.append(emoji)
|
|
188
|
+
segs.append(info.get("display", action_name))
|
|
189
|
+
if info.get("type"):
|
|
190
|
+
segs.append(f"({info['type']})")
|
|
191
|
+
privs = info.get("privileges", [])
|
|
192
|
+
if privs:
|
|
193
|
+
segs.append(f"({', '.join(privs)})")
|
|
194
|
+
nb = info.get("notebook_id")
|
|
195
|
+
if info.get("type") == "NOTEBOOK" and nb:
|
|
196
|
+
segs.append(f"NB {nb[:8]}...")
|
|
197
|
+
icon = info.get("icon")
|
|
198
|
+
if icon:
|
|
199
|
+
segs.append(f"⚡️ {icon}")
|
|
200
|
+
segs = [sanitize_mermaid_label(s) for s in segs if s]
|
|
201
|
+
action_label = "\\n".join(segs)
|
|
202
|
+
if show_in_ui:
|
|
203
|
+
lines.append(f" {source_id} --> {target_id} : {action_label}")
|
|
204
|
+
else:
|
|
205
|
+
lines.append(f" {source_id} --> {target_id} : {action_label}\\nhidden")
|
|
206
|
+
|
|
207
|
+
lines.append("")
|
|
208
|
+
return "\n".join(lines)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_legend_html() -> str:
|
|
212
|
+
"""Return legend HTML using LEGEND_ROWS constant."""
|
|
213
|
+
rows = "\n".join(f" <tr><td>{k}</td><td>{v}</td></tr>" for k, v in LEGEND_ROWS)
|
|
214
|
+
return (
|
|
215
|
+
' <div class="legend" style="text-align:left; max-width:400px; margin:0 0 20px 0; background:#fafafa; border:1px solid #ddd; padding:12px; border-radius:6px; font-size:0.9em;">\n'
|
|
216
|
+
" <strong>Legend</strong>\n"
|
|
217
|
+
' <table class="legend-table">\n'
|
|
218
|
+
f"{rows}\n"
|
|
219
|
+
" </table>\n"
|
|
220
|
+
" </div>"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def generate_html_with_mermaid(
|
|
225
|
+
workflow_data: Dict[str, Any], mermaid_code: str, include_legend: bool = True
|
|
226
|
+
) -> str:
|
|
227
|
+
"""Generate HTML document for a workflow diagram.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
workflow_data: Workflow JSON
|
|
231
|
+
mermaid_code: Mermaid diagram source
|
|
232
|
+
include_legend: Whether to include legend block
|
|
233
|
+
"""
|
|
234
|
+
workflow_name = workflow_data.get("name", "Workflow")
|
|
235
|
+
workflow_description = workflow_data.get("description", "")
|
|
236
|
+
legend_block = build_legend_html() if include_legend else ""
|
|
237
|
+
# Precompute optional HTML fragments to avoid nested f-strings inside expression braces
|
|
238
|
+
description_html = (
|
|
239
|
+
f'<p class="workflow-description">{workflow_description}</p>'
|
|
240
|
+
if workflow_description
|
|
241
|
+
else ""
|
|
242
|
+
)
|
|
243
|
+
workspace_value = workflow_data.get("workspace")
|
|
244
|
+
if workspace_value:
|
|
245
|
+
workspace_html = f"<p><strong>Workspace:</strong> {workspace_value}</p>"
|
|
246
|
+
else:
|
|
247
|
+
workspace_html = ""
|
|
248
|
+
return f"""<!DOCTYPE html>
|
|
249
|
+
<html lang=\"en\">
|
|
250
|
+
<head>
|
|
251
|
+
<meta charset=\"UTF-8\">
|
|
252
|
+
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
|
|
253
|
+
<title>Workflow Preview: {workflow_name}</title>
|
|
254
|
+
<script src=\"https://cdn.jsdelivr.net/npm/mermaid@10.6.1/dist/mermaid.min.js\"></script>
|
|
255
|
+
<style>
|
|
256
|
+
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }}
|
|
257
|
+
.container {{ max-width: 1200px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
|
|
258
|
+
.header {{ margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 10px; }}
|
|
259
|
+
.workflow-title {{ color: #333; margin: 0; }}
|
|
260
|
+
.workflow-description {{ color: #666; margin: 5px 0 0 0; }}
|
|
261
|
+
.diagram-container {{ text-align: center; margin: 20px 0; }}
|
|
262
|
+
.metadata {{ margin-top: 20px; font-size: 0.9em; color: #666; }}
|
|
263
|
+
.legend-table {{ width: 100%; border-collapse: collapse; margin: 8px 0 0 0; }}
|
|
264
|
+
.legend-table td {{ padding: 4px 8px; border-bottom: 1px solid #eee; vertical-align: top; }}
|
|
265
|
+
.legend-table td:first-child {{ font-family: monospace; font-weight: bold; width: 120px; }}
|
|
266
|
+
</style>
|
|
267
|
+
</head>
|
|
268
|
+
<body>
|
|
269
|
+
<div class=\"container\">
|
|
270
|
+
<div class=\"header\">
|
|
271
|
+
<h1 class=\"workflow-title\">{workflow_name}</h1>
|
|
272
|
+
{description_html}
|
|
273
|
+
</div>
|
|
274
|
+
<div class=\"diagram-container\">
|
|
275
|
+
<div class=\"mermaid\">\n{mermaid_code}\n </div>
|
|
276
|
+
</div>
|
|
277
|
+
{legend_block}
|
|
278
|
+
<div class=\"metadata\">
|
|
279
|
+
<h3>Workflow Details</h3>
|
|
280
|
+
<p><strong>States:</strong> {len(workflow_data.get('states', []))}</p>
|
|
281
|
+
<p><strong>Actions:</strong> {len(workflow_data.get('actions', []))}</p>
|
|
282
|
+
{workspace_html}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
<script>mermaid.initialize({{ startOnLoad: true, theme: 'default', fontFamily: 'Arial, sans-serif' }});</script>
|
|
286
|
+
</body>
|
|
287
|
+
</html>"""
|