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.
Files changed (74) hide show
  1. slcli/__init__.py +1 -0
  2. slcli/__main__.py +23 -0
  3. slcli/_version.py +4 -0
  4. slcli/asset_click.py +1289 -0
  5. slcli/cli_formatters.py +218 -0
  6. slcli/cli_utils.py +504 -0
  7. slcli/comment_click.py +602 -0
  8. slcli/completion_click.py +418 -0
  9. slcli/config.py +81 -0
  10. slcli/config_click.py +498 -0
  11. slcli/dff_click.py +979 -0
  12. slcli/dff_decorators.py +24 -0
  13. slcli/example_click.py +404 -0
  14. slcli/example_loader.py +274 -0
  15. slcli/example_provisioner.py +2777 -0
  16. slcli/examples/README.md +134 -0
  17. slcli/examples/_schema/schema-v1.0.json +169 -0
  18. slcli/examples/demo-complete-workflow/README.md +323 -0
  19. slcli/examples/demo-complete-workflow/config.yaml +638 -0
  20. slcli/examples/demo-test-plans/README.md +132 -0
  21. slcli/examples/demo-test-plans/config.yaml +154 -0
  22. slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
  23. slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
  24. slcli/examples/exercise-7-1-test-plans/README.md +93 -0
  25. slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
  26. slcli/examples/spec-compliance-notebooks/README.md +140 -0
  27. slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
  28. slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
  29. slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
  30. slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
  31. slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  32. slcli/feed_click.py +892 -0
  33. slcli/file_click.py +932 -0
  34. slcli/function_click.py +1400 -0
  35. slcli/function_templates.py +85 -0
  36. slcli/main.py +406 -0
  37. slcli/mcp_click.py +269 -0
  38. slcli/mcp_server.py +748 -0
  39. slcli/notebook_click.py +1770 -0
  40. slcli/platform.py +345 -0
  41. slcli/policy_click.py +679 -0
  42. slcli/policy_utils.py +411 -0
  43. slcli/profiles.py +411 -0
  44. slcli/response_handlers.py +359 -0
  45. slcli/routine_click.py +763 -0
  46. slcli/skill_click.py +253 -0
  47. slcli/skills/slcli/SKILL.md +713 -0
  48. slcli/skills/slcli/references/analysis-recipes.md +474 -0
  49. slcli/skills/slcli/references/filtering.md +236 -0
  50. slcli/skills/systemlink-webapp/SKILL.md +744 -0
  51. slcli/skills/systemlink-webapp/references/deployment.md +123 -0
  52. slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
  53. slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
  54. slcli/ssl_trust.py +93 -0
  55. slcli/system_click.py +2216 -0
  56. slcli/table_utils.py +124 -0
  57. slcli/tag_click.py +794 -0
  58. slcli/templates_click.py +599 -0
  59. slcli/testmonitor_click.py +1667 -0
  60. slcli/universal_handlers.py +305 -0
  61. slcli/user_click.py +1218 -0
  62. slcli/utils.py +832 -0
  63. slcli/web_editor.py +295 -0
  64. slcli/webapp_click.py +981 -0
  65. slcli/workflow_preview.py +287 -0
  66. slcli/workflows_click.py +988 -0
  67. slcli/workitem_click.py +2258 -0
  68. slcli/workspace_click.py +576 -0
  69. slcli/workspace_utils.py +206 -0
  70. systemlink_cli-1.3.1.dist-info/METADATA +20 -0
  71. systemlink_cli-1.3.1.dist-info/RECORD +74 -0
  72. systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
  73. systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
  74. 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>"""