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/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)
|