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
slcli/platform.py ADDED
@@ -0,0 +1,345 @@
1
+ """Platform detection and feature gating for SystemLink CLI.
2
+
3
+ This module provides utilities to detect and manage the target platform
4
+ (SystemLink Enterprise vs SystemLink Server) and gate features accordingly.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import sys
10
+ from functools import lru_cache
11
+ from typing import Any, Dict
12
+
13
+ import click
14
+ import keyring
15
+ import requests
16
+
17
+ from .utils import ExitCodes, get_ssl_verify
18
+
19
+
20
+ # Platform identifiers
21
+ PLATFORM_SLE = "SLE" # SystemLink Enterprise (cloud)
22
+ PLATFORM_SLS = "SLS" # SystemLink Server (on-premises)
23
+ PLATFORM_UNKNOWN = "unknown"
24
+
25
+ # Feature matrix: maps features to platform availability
26
+ PLATFORM_FEATURES: Dict[str, Dict[str, bool]] = {
27
+ PLATFORM_SLE: {
28
+ "service_accounts": True,
29
+ "workorder_service": True,
30
+ "dynamic_form_fields": True,
31
+ "function_execution": True,
32
+ "templates": True,
33
+ "workflows": True,
34
+ "webapp": True,
35
+ },
36
+ PLATFORM_SLS: {
37
+ "service_accounts": False,
38
+ "workorder_service": False,
39
+ "dynamic_form_fields": False,
40
+ "function_execution": False,
41
+ "templates": False, # Uses workorder service
42
+ "workflows": False, # Uses workorder service
43
+ "webapp": True, # May be available
44
+ },
45
+ }
46
+
47
+ # Human-readable feature names for error messages
48
+ FEATURE_DISPLAY_NAMES: Dict[str, str] = {
49
+ "service_accounts": "Service Accounts",
50
+ "workorder_service": "Work Order Service",
51
+ "dynamic_form_fields": "Dynamic Form Fields",
52
+ "function_execution": "Function Execution",
53
+ "templates": "Test Plan Templates",
54
+ "workflows": "Workflows",
55
+ "webapp": "Web Applications",
56
+ }
57
+
58
+
59
+ def _get_keyring_config() -> Dict[str, Any]:
60
+ """Attempt to read a single JSON config entry from keyring.
61
+
62
+ Returns:
63
+ Dictionary with config values or empty dict on failure.
64
+ """
65
+ try:
66
+ cfg_text = keyring.get_password("systemlink-cli", "SYSTEMLINK_CONFIG")
67
+ if not cfg_text:
68
+ return {}
69
+ parsed = json.loads(cfg_text)
70
+ if isinstance(parsed, dict):
71
+ return parsed
72
+ except Exception: # noqa: BLE001
73
+ # Intentionally catch all exceptions: keyring access can fail for many reasons
74
+ # (missing backend, corrupted data, permission issues, JSON decode errors).
75
+ # None of these should prevent CLI operation - we just return empty config.
76
+ pass
77
+ return {}
78
+
79
+
80
+ def detect_platform(api_url: str, api_key: str) -> str:
81
+ """Detect the SystemLink platform type by probing endpoints.
82
+
83
+ Detection strategy:
84
+ 1. Try SLE-only endpoint (/niworkorder/v1/query-testplan-templates)
85
+ - If accessible -> SLE
86
+ 2. Check URL pattern (*.systemlink.io, *.lifecyclesolutions.ni.com)
87
+ - If matches -> SLE
88
+ 3. Default to SLS for on-premises/custom URLs
89
+
90
+ Args:
91
+ api_url: The SystemLink API base URL
92
+ api_key: The API key for authentication
93
+
94
+ Returns:
95
+ Platform identifier (PLATFORM_SLE, PLATFORM_SLS, or PLATFORM_UNKNOWN)
96
+ """
97
+ headers = {
98
+ "x-ni-api-key": api_key,
99
+ "Content-Type": "application/json",
100
+ "User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
101
+ }
102
+ ssl_verify = get_ssl_verify()
103
+
104
+ # Strategy 1: Probe SLE-only endpoint (Work Order service)
105
+ try:
106
+ # This endpoint only exists on SLE
107
+ workorder_url = f"{api_url}/niworkorder/v1/query-testplan-templates"
108
+ resp = requests.post(
109
+ workorder_url,
110
+ headers=headers,
111
+ json={"take": 1},
112
+ verify=ssl_verify,
113
+ timeout=10,
114
+ )
115
+ # If we get a 200 or 400 (bad request but endpoint exists), it's SLE
116
+ if resp.status_code in (200, 400):
117
+ return PLATFORM_SLE
118
+ # 404 means endpoint doesn't exist -> likely SLS
119
+ if resp.status_code == 404:
120
+ return PLATFORM_SLS
121
+ except requests.RequestException:
122
+ # Connection error - continue with other detection methods
123
+ pass
124
+
125
+ # Strategy 2: URL pattern matching
126
+ # SLE (cloud and hosted) service has specific URL patterns
127
+ api_url_lower = api_url.lower()
128
+ sle_patterns = [
129
+ "api.systemlink.io", # SLE production
130
+ "-api.lifecyclesolutions.ni.com", # SLE dev/demo with -api suffix
131
+ "dev-api.lifecyclesolutions",
132
+ "demo-api.lifecyclesolutions",
133
+ ]
134
+ for pattern in sle_patterns:
135
+ if pattern in api_url_lower:
136
+ return PLATFORM_SLE
137
+
138
+ # Strategy 3: Default to SLS for on-premises deployments
139
+ # This includes on-prem servers that may use *.systemlink.io subdomains
140
+ return PLATFORM_SLS
141
+
142
+
143
+ def _detect_platform_from_url(api_url: str) -> str:
144
+ """Detect platform from URL pattern without making network requests.
145
+
146
+ This is a lightweight detection for use when environment variables
147
+ are set and we need quick platform detection.
148
+
149
+ SLE (SystemLink Enterprise Cloud) URLs typically contain:
150
+ - api.systemlink.io (production)
151
+ - dev-api.lifecyclesolutions.ni.com (development)
152
+ - demo-api.lifecyclesolutions.ni.com (demo)
153
+
154
+ On-premises SystemLink Server (SLS) instances may use custom domains
155
+ or even *.systemlink.io subdomains (like base.systemlink.io).
156
+
157
+ Args:
158
+ api_url: The SystemLink API base URL
159
+
160
+ Returns:
161
+ Platform identifier: PLATFORM_SLE or PLATFORM_SLS.
162
+ Note: This function never returns PLATFORM_UNKNOWN - it defaults to SLS.
163
+ """
164
+ api_url_lower = api_url.lower()
165
+
166
+ # SLE cloud service has specific URL patterns
167
+ sle_patterns = [
168
+ "api.systemlink.io", # SLE production
169
+ "-api.lifecyclesolutions.ni.com", # SLE dev/demo with -api suffix
170
+ "dev-api.lifecyclesolutions",
171
+ "demo-api.lifecyclesolutions",
172
+ ]
173
+ for pattern in sle_patterns:
174
+ if pattern in api_url_lower:
175
+ return PLATFORM_SLE
176
+
177
+ # Default to SLS for on-premises/custom URLs
178
+ # This includes on-prem servers that may use *.systemlink.io subdomains
179
+ return PLATFORM_SLS
180
+
181
+
182
+ @lru_cache(maxsize=1)
183
+ def get_platform() -> str:
184
+ """Get the current platform from stored configuration or environment.
185
+
186
+ Detection priority:
187
+ 1. SYSTEMLINK_PLATFORM environment variable (explicit, most reliable)
188
+ 2. Stored platform from keyring config (set during login via endpoint probing)
189
+ 3. URL pattern matching (fallback, less reliable)
190
+ 4. Return PLATFORM_UNKNOWN if all methods fail
191
+
192
+ Note: Results are cached for performance. Use clear_platform_cache() to reset.
193
+
194
+ Returns:
195
+ Platform identifier (PLATFORM_SLE, PLATFORM_SLS, or PLATFORM_UNKNOWN)
196
+ """
197
+ # Priority 1: Explicit platform environment variable (most reliable)
198
+ # This allows users/tests to explicitly specify the platform
199
+ env_platform = os.environ.get("SYSTEMLINK_PLATFORM", "").upper()
200
+ if env_platform in (PLATFORM_SLE, PLATFORM_SLS):
201
+ return env_platform
202
+
203
+ # Priority 2: Stored platform from keyring config (set during login)
204
+ # This was detected via endpoint probing, which is reliable
205
+ cfg = _get_keyring_config()
206
+ if cfg:
207
+ platform = cfg.get("platform", "")
208
+ if platform in (PLATFORM_SLE, PLATFORM_SLS):
209
+ return platform
210
+
211
+ # Priority 3: URL pattern matching (fallback, less reliable)
212
+ # Only used when env vars are set but no explicit platform is provided
213
+ env_url = os.environ.get("SYSTEMLINK_API_URL")
214
+ if env_url:
215
+ return _detect_platform_from_url(env_url)
216
+
217
+ return PLATFORM_UNKNOWN
218
+
219
+
220
+ def clear_platform_cache() -> None:
221
+ """Clear the cached platform result.
222
+
223
+ Call this when the platform configuration changes (e.g., after login/logout)
224
+ to ensure the next get_platform() call re-detects the platform.
225
+ """
226
+ get_platform.cache_clear()
227
+
228
+
229
+ def has_feature(feature_name: str) -> bool:
230
+ """Check if a feature is available on the current platform.
231
+
232
+ Args:
233
+ feature_name: The feature to check (e.g., 'dynamic_form_fields')
234
+
235
+ Returns:
236
+ True if the feature is available, False otherwise.
237
+ Returns True if platform is unknown (graceful degradation).
238
+ """
239
+ platform = get_platform()
240
+
241
+ # If platform is unknown, allow all features (fail later if actually unavailable)
242
+ if platform == PLATFORM_UNKNOWN:
243
+ return True
244
+
245
+ platform_features = PLATFORM_FEATURES.get(platform, {})
246
+ return platform_features.get(feature_name, True) # Default to available
247
+
248
+
249
+ def require_feature(feature_name: str) -> None:
250
+ """Require a feature to be available, exit gracefully if not.
251
+
252
+ This function should be called at the start of commands that require
253
+ platform-specific features. It will display a helpful error message
254
+ and exit if the feature is not available.
255
+
256
+ Args:
257
+ feature_name: The feature to require (e.g., 'dynamic_form_fields')
258
+ """
259
+ if has_feature(feature_name):
260
+ return
261
+
262
+ platform = get_platform()
263
+ feature_display = FEATURE_DISPLAY_NAMES.get(feature_name, feature_name)
264
+ platform_display = "SystemLink Server" if platform == PLATFORM_SLS else platform
265
+
266
+ click.echo(
267
+ f"✗ Error: {feature_display} is not available on {platform_display}.",
268
+ err=True,
269
+ )
270
+ click.echo(
271
+ " This feature requires SystemLink Enterprise (SLE).",
272
+ err=True,
273
+ )
274
+ sys.exit(ExitCodes.INVALID_INPUT)
275
+
276
+
277
+ def get_platform_info() -> Dict[str, Any]:
278
+ """Get detailed information about the current platform configuration.
279
+
280
+ Returns:
281
+ Dictionary with platform info including URL, platform type, and features.
282
+ """
283
+ from .utils import get_api_key, get_base_url, get_web_url
284
+
285
+ # Use profile-aware functions instead of keyring directly
286
+ try:
287
+ api_url = get_base_url()
288
+ except Exception:
289
+ api_url = "Not configured"
290
+
291
+ try:
292
+ web_url = get_web_url()
293
+ except Exception:
294
+ web_url = "Not configured"
295
+
296
+ try:
297
+ api_key = get_api_key()
298
+ logged_in = bool(api_key)
299
+ except Exception:
300
+ logged_in = False
301
+
302
+ # Get platform from profile or keyring config
303
+ from .profiles import get_active_profile
304
+
305
+ active_profile = get_active_profile()
306
+ if active_profile and active_profile.platform:
307
+ platform = active_profile.platform
308
+ else:
309
+ # Fall back to keyring config
310
+ cfg = _get_keyring_config()
311
+ platform = cfg.get("platform", PLATFORM_UNKNOWN)
312
+
313
+ info: Dict[str, Any] = {
314
+ "api_url": api_url,
315
+ "web_url": web_url,
316
+ "platform": platform,
317
+ "platform_display": _get_platform_display_name(platform),
318
+ "logged_in": logged_in,
319
+ }
320
+
321
+ # Add feature availability if platform is known
322
+ if platform in PLATFORM_FEATURES:
323
+ info["features"] = {}
324
+ for feature, available in PLATFORM_FEATURES[platform].items():
325
+ display_name = FEATURE_DISPLAY_NAMES.get(feature, feature)
326
+ info["features"][display_name] = available
327
+
328
+ return info
329
+
330
+
331
+ def _get_platform_display_name(platform: str) -> str:
332
+ """Get human-readable platform name.
333
+
334
+ Args:
335
+ platform: Platform identifier
336
+
337
+ Returns:
338
+ Human-readable platform name
339
+ """
340
+ names = {
341
+ PLATFORM_SLE: "SystemLink Enterprise",
342
+ PLATFORM_SLS: "SystemLink Server",
343
+ PLATFORM_UNKNOWN: "Unknown",
344
+ }
345
+ return names.get(platform, platform)