systemlink-cli 1.4.2__tar.gz → 1.4.4__tar.gz
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.
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/PKG-INFO +1 -1
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/pyproject.toml +1 -1
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/_version.py +1 -1
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/config_click.py +8 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/demo-complete-workflow/README.md +1 -3
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/file_click.py +136 -11
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/main.py +10 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/platform.py +107 -53
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/user_click.py +1 -1
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/LICENSE +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/dff-editor/editor.js +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/dff-editor/index.html +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/__init__.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/__main__.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/config.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/example_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/function_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/mcp_server.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/notebook_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/profiles.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/slcli/SKILL.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/system_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/utils.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/web_editor.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/webapp_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/workspace_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.4.
|
|
3
|
+
version = "1.4.4"
|
|
4
4
|
description = "SystemLink Integrator CLI - cross-platform CLI for SystemLink workflows and templates."
|
|
5
5
|
authors = ["Fred Visser <fred.visser@emerson.com>"]
|
|
6
6
|
packages = [{ include = "slcli" }]
|
|
@@ -64,6 +64,7 @@ def _add_profile_impl(
|
|
|
64
64
|
elif not url.startswith("https://"):
|
|
65
65
|
click.echo("⚠️ Warning: Adding HTTPS protocol to URL.")
|
|
66
66
|
url = f"https://{url}"
|
|
67
|
+
url = url.rstrip("/")
|
|
67
68
|
|
|
68
69
|
# Get API key - either from flag or prompt
|
|
69
70
|
if not api_key:
|
|
@@ -86,6 +87,7 @@ def _add_profile_impl(
|
|
|
86
87
|
elif not web_url.startswith("https://"):
|
|
87
88
|
click.echo("⚠️ Warning: Adding HTTPS protocol to web URL.")
|
|
88
89
|
web_url = f"https://{web_url}"
|
|
90
|
+
web_url = web_url.rstrip("/")
|
|
89
91
|
|
|
90
92
|
# Detect platform type and check service status
|
|
91
93
|
click.echo("Checking server connectivity and services...")
|
|
@@ -113,6 +115,12 @@ def _add_profile_impl(
|
|
|
113
115
|
elif status["auth_valid"] is True:
|
|
114
116
|
click.echo(" API key: ✓ Authorized")
|
|
115
117
|
|
|
118
|
+
if status.get("elasticsearch_available") is False:
|
|
119
|
+
click.echo(" File query: query-files-linq (Elasticsearch unavailable)")
|
|
120
|
+
click.echo(
|
|
121
|
+
" 'slcli file list' will fall back automatically; 'slcli file query' requires search-files."
|
|
122
|
+
)
|
|
123
|
+
|
|
116
124
|
# Report individual service status
|
|
117
125
|
services = status.get("services", {})
|
|
118
126
|
problem_services = [
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/demo-complete-workflow/README.md
RENAMED
|
@@ -303,9 +303,7 @@ slcli result list -w <workspace>
|
|
|
303
303
|
|
|
304
304
|
## Additional Resources
|
|
305
305
|
|
|
306
|
-
- **SystemLink API Documentation**: https://
|
|
307
|
-
- **Test Monitor Specs**: https://dev-api.lifecyclesolutions.ni.com/nitestmonitor/swagger/
|
|
308
|
-
- **Work Item API**: https://dev-api.lifecyclesolutions.ni.com/niworkitem/swagger/
|
|
306
|
+
- **SystemLink API Documentation**: https://demo-api.lifecyclesolutions.ni.com/niapis/
|
|
309
307
|
- **systemlink-cli GitHub**: https://github.com/ni/systemlink-cli
|
|
310
308
|
|
|
311
309
|
## Contact & Support
|
|
@@ -10,9 +10,10 @@ from typing import Any, Dict, List, Optional
|
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
12
|
import questionary
|
|
13
|
+
import requests as requests_lib
|
|
13
14
|
|
|
14
15
|
from .cli_utils import validate_output_format
|
|
15
|
-
from .universal_handlers import UniversalResponseHandler
|
|
16
|
+
from .universal_handlers import FilteredResponse, UniversalResponseHandler
|
|
16
17
|
from .utils import (
|
|
17
18
|
ExitCodes,
|
|
18
19
|
format_success,
|
|
@@ -29,6 +30,120 @@ def _get_file_service_url() -> str:
|
|
|
29
30
|
return f"{get_base_url()}/nifile/v1/service-groups/Default"
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
def _get_search_files_url() -> str:
|
|
34
|
+
"""Get the search-files endpoint URL."""
|
|
35
|
+
return f"{_get_file_service_url()}/search-files"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_search_files_unavailable(exc: requests_lib.HTTPError) -> bool:
|
|
39
|
+
"""Return whether the search-files endpoint is unavailable."""
|
|
40
|
+
return exc.response is not None and exc.response.status_code in (404, 501)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _exit_search_files_required() -> None:
|
|
44
|
+
"""Exit with a user-facing message when search-files is unavailable."""
|
|
45
|
+
click.echo(
|
|
46
|
+
"✗ File query requires the search-files endpoint, but Elasticsearch is not available.",
|
|
47
|
+
err=True,
|
|
48
|
+
)
|
|
49
|
+
click.echo(
|
|
50
|
+
" Use 'slcli file list' for automatic fallback to query-files-linq.",
|
|
51
|
+
err=True,
|
|
52
|
+
)
|
|
53
|
+
sys.exit(ExitCodes.INVALID_INPUT)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _search_files_with_fallback(
|
|
57
|
+
payload: Dict[str, Any],
|
|
58
|
+
workspace_id: Optional[str],
|
|
59
|
+
name_filter: Optional[str],
|
|
60
|
+
id_filter: Optional[str],
|
|
61
|
+
) -> Any:
|
|
62
|
+
"""Try search-files endpoint, fall back to query-files-linq if unavailable.
|
|
63
|
+
|
|
64
|
+
The search-files endpoint requires additional software (DataFinder) to be
|
|
65
|
+
installed. If it is not available (HTTP 404), we fall back to query-files-linq.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
payload: The search-files request payload
|
|
69
|
+
workspace_id: Optional workspace ID filter
|
|
70
|
+
name_filter: Optional name filter string
|
|
71
|
+
id_filter: Optional comma-separated file IDs
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Response object (real or FilteredResponse)
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
return make_api_request(
|
|
78
|
+
"POST", _get_search_files_url(), payload=payload, handle_errors=False
|
|
79
|
+
)
|
|
80
|
+
except requests_lib.HTTPError as exc:
|
|
81
|
+
if _is_search_files_unavailable(exc):
|
|
82
|
+
return _query_files_linq_fallback(
|
|
83
|
+
take=payload.get("take", 25),
|
|
84
|
+
workspace_id=workspace_id,
|
|
85
|
+
name_filter=name_filter,
|
|
86
|
+
id_filter=id_filter,
|
|
87
|
+
)
|
|
88
|
+
raise
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _query_files_linq_fallback(
|
|
92
|
+
take: int,
|
|
93
|
+
workspace_id: Optional[str],
|
|
94
|
+
name_filter: Optional[str],
|
|
95
|
+
id_filter: Optional[str],
|
|
96
|
+
) -> Any:
|
|
97
|
+
"""List files using query-files-linq as a fallback when search-files is unavailable.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
take: Maximum number of files to return
|
|
101
|
+
workspace_id: Optional workspace ID filter
|
|
102
|
+
name_filter: Optional name filter string (applied client-side)
|
|
103
|
+
id_filter: Optional comma-separated file IDs
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Response or FilteredResponse with availableFiles data
|
|
107
|
+
"""
|
|
108
|
+
base_url = _get_file_service_url()
|
|
109
|
+
url = f"{base_url}/query-files-linq"
|
|
110
|
+
|
|
111
|
+
# Build LINQ-style filter
|
|
112
|
+
filter_parts: List[str] = []
|
|
113
|
+
|
|
114
|
+
if id_filter:
|
|
115
|
+
ids = [fid.strip() for fid in id_filter.split(",")]
|
|
116
|
+
id_conditions = [f'id = "{fid}"' for fid in ids]
|
|
117
|
+
filter_parts.append("(" + " or ".join(id_conditions) + ")")
|
|
118
|
+
|
|
119
|
+
linq_payload: Dict[str, Any] = {}
|
|
120
|
+
|
|
121
|
+
# Request more if we need to filter client-side by name
|
|
122
|
+
if name_filter:
|
|
123
|
+
linq_payload["take"] = max(take * 4, 1000)
|
|
124
|
+
else:
|
|
125
|
+
linq_payload["take"] = take
|
|
126
|
+
|
|
127
|
+
if filter_parts:
|
|
128
|
+
linq_payload["filter"] = " and ".join(filter_parts)
|
|
129
|
+
|
|
130
|
+
# Workspace filtering via query parameter
|
|
131
|
+
if workspace_id:
|
|
132
|
+
url += f"?workspace={workspace_id}"
|
|
133
|
+
|
|
134
|
+
resp = make_api_request("POST", url, payload=linq_payload)
|
|
135
|
+
|
|
136
|
+
# Client-side name filtering (query-files-linq doesn't support name wildcards)
|
|
137
|
+
if name_filter:
|
|
138
|
+
data = resp.json()
|
|
139
|
+
files = data.get("availableFiles", [])
|
|
140
|
+
needle = name_filter.lower()
|
|
141
|
+
filtered = [f for f in files if needle in _get_file_name(f).lower()][:take]
|
|
142
|
+
return FilteredResponse({"availableFiles": filtered})
|
|
143
|
+
|
|
144
|
+
return resp
|
|
145
|
+
|
|
146
|
+
|
|
32
147
|
def _format_file_size(size_bytes: Optional[int]) -> str:
|
|
33
148
|
"""Format file size in human-readable format.
|
|
34
149
|
|
|
@@ -178,9 +293,6 @@ def register_file_commands(cli: Any) -> None:
|
|
|
178
293
|
"""
|
|
179
294
|
format_output = validate_output_format(format)
|
|
180
295
|
|
|
181
|
-
# Use search-files endpoint for better performance
|
|
182
|
-
url = f"{_get_file_service_url()}/search-files"
|
|
183
|
-
|
|
184
296
|
try:
|
|
185
297
|
# Resolve workspace name to ID if needed
|
|
186
298
|
workspace_id = None
|
|
@@ -189,7 +301,7 @@ def register_file_commands(cli: Any) -> None:
|
|
|
189
301
|
workspace_map = get_workspace_map()
|
|
190
302
|
workspace_id = resolve_workspace_filter(workspace, workspace_map)
|
|
191
303
|
|
|
192
|
-
# Build search filter
|
|
304
|
+
# Build search filter for search-files endpoint
|
|
193
305
|
filter_parts = []
|
|
194
306
|
|
|
195
307
|
if workspace_id:
|
|
@@ -221,7 +333,13 @@ def register_file_commands(cli: Any) -> None:
|
|
|
221
333
|
if filter_parts:
|
|
222
334
|
payload["filter"] = " AND ".join(filter_parts)
|
|
223
335
|
|
|
224
|
-
|
|
336
|
+
# Try search-files, fall back to query-files-linq if unavailable
|
|
337
|
+
resp = _search_files_with_fallback(
|
|
338
|
+
payload=payload,
|
|
339
|
+
workspace_id=workspace_id,
|
|
340
|
+
name_filter=name_filter,
|
|
341
|
+
id_filter=id_filter,
|
|
342
|
+
)
|
|
225
343
|
|
|
226
344
|
def file_formatter(file_item: dict) -> list:
|
|
227
345
|
name = _get_file_name(file_item)
|
|
@@ -567,11 +685,8 @@ def register_file_commands(cli: Any) -> None:
|
|
|
567
685
|
"""
|
|
568
686
|
format_output = validate_output_format(format)
|
|
569
687
|
|
|
570
|
-
# Use search-files endpoint for better performance
|
|
571
|
-
url = f"{_get_file_service_url()}/search-files"
|
|
572
|
-
|
|
573
688
|
try:
|
|
574
|
-
# Build request body
|
|
689
|
+
# Build request body for search-files
|
|
575
690
|
query_body: Dict[str, Any] = {
|
|
576
691
|
"take": take if format_output.lower() == "json" else (take if take != 25 else 1000),
|
|
577
692
|
"orderByDescending": descending,
|
|
@@ -584,6 +699,7 @@ def register_file_commands(cli: Any) -> None:
|
|
|
584
699
|
filter_parts.append(filter_query)
|
|
585
700
|
|
|
586
701
|
# Resolve workspace name to ID if needed
|
|
702
|
+
workspace_id = None
|
|
587
703
|
workspace = get_effective_workspace(workspace)
|
|
588
704
|
if workspace:
|
|
589
705
|
workspace_map = get_workspace_map()
|
|
@@ -598,7 +714,12 @@ def register_file_commands(cli: Any) -> None:
|
|
|
598
714
|
else:
|
|
599
715
|
query_body["orderBy"] = "updated"
|
|
600
716
|
|
|
601
|
-
resp = make_api_request(
|
|
717
|
+
resp = make_api_request(
|
|
718
|
+
"POST",
|
|
719
|
+
_get_search_files_url(),
|
|
720
|
+
payload=query_body,
|
|
721
|
+
handle_errors=False,
|
|
722
|
+
)
|
|
602
723
|
|
|
603
724
|
def file_formatter(file_item: dict) -> list:
|
|
604
725
|
name = _get_file_name(file_item)
|
|
@@ -620,6 +741,10 @@ def register_file_commands(cli: Any) -> None:
|
|
|
620
741
|
page_size=take,
|
|
621
742
|
)
|
|
622
743
|
|
|
744
|
+
except requests_lib.HTTPError as exc:
|
|
745
|
+
if _is_search_files_unavailable(exc):
|
|
746
|
+
_exit_search_files_required()
|
|
747
|
+
handle_api_error(exc)
|
|
623
748
|
except Exception as exc:
|
|
624
749
|
handle_api_error(exc)
|
|
625
750
|
|
|
@@ -368,6 +368,15 @@ def info(format: str, skip_health: bool) -> None:
|
|
|
368
368
|
workspace_display = truncate(workspace)
|
|
369
369
|
click.echo(f"│ Workspace: {workspace_display:<48}│")
|
|
370
370
|
|
|
371
|
+
file_query_endpoint = platform_info.get("file_query_endpoint")
|
|
372
|
+
if file_query_endpoint:
|
|
373
|
+
if platform_info.get("elasticsearch_available") is False:
|
|
374
|
+
file_query_display = f"{file_query_endpoint} (Elasticsearch unavailable)"
|
|
375
|
+
else:
|
|
376
|
+
file_query_display = str(file_query_endpoint)
|
|
377
|
+
file_query_display = truncate(file_query_display)
|
|
378
|
+
click.echo(f"│ File Query:{file_query_display:<48}│")
|
|
379
|
+
|
|
371
380
|
# Service Health section (only when services were checked)
|
|
372
381
|
services = platform_info.get("services")
|
|
373
382
|
if services:
|
|
@@ -377,6 +386,7 @@ def info(format: str, skip_health: bool) -> None:
|
|
|
377
386
|
|
|
378
387
|
status_display = {
|
|
379
388
|
"ok": ("✓", "OK"),
|
|
389
|
+
"fallback": ("!", "Fallback (no Elasticsearch)"),
|
|
380
390
|
"unauthorized": ("✗", "Unauthorized"),
|
|
381
391
|
"not_found": ("—", "Not available"),
|
|
382
392
|
"error": ("✗", "Error"),
|
|
@@ -56,6 +56,9 @@ FEATURE_DISPLAY_NAMES: Dict[str, str] = {
|
|
|
56
56
|
"webapp": "Web Applications",
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
FILE_SEARCH_PATH = "/nifile/v1/service-groups/Default/search-files"
|
|
60
|
+
FILE_QUERY_LINQ_PATH = "/nifile/v1/service-groups/Default/query-files-linq"
|
|
61
|
+
|
|
59
62
|
|
|
60
63
|
def _get_keyring_config() -> Dict[str, Any]:
|
|
61
64
|
"""Attempt to read a single JSON config entry from keyring.
|
|
@@ -84,7 +87,6 @@ def detect_platform(api_url: str, api_key: str) -> str:
|
|
|
84
87
|
Uses check_service_status to probe services and determine:
|
|
85
88
|
- Platform type (SLE vs SLS)
|
|
86
89
|
- Server reachability
|
|
87
|
-
Falls back to URL pattern matching when probe is inconclusive.
|
|
88
90
|
|
|
89
91
|
Args:
|
|
90
92
|
api_url: The SystemLink API base URL
|
|
@@ -98,45 +100,6 @@ def detect_platform(api_url: str, api_key: str) -> str:
|
|
|
98
100
|
return status["platform"]
|
|
99
101
|
|
|
100
102
|
|
|
101
|
-
def _detect_platform_from_url(api_url: str) -> str:
|
|
102
|
-
"""Detect platform from URL pattern without making network requests.
|
|
103
|
-
|
|
104
|
-
This is a lightweight detection for use when environment variables
|
|
105
|
-
are set and we need quick platform detection.
|
|
106
|
-
|
|
107
|
-
SLE (SystemLink Enterprise Cloud) URLs typically contain:
|
|
108
|
-
- api.systemlink.io (production)
|
|
109
|
-
- dev-api.lifecyclesolutions.ni.com (development)
|
|
110
|
-
- demo-api.lifecyclesolutions.ni.com (demo)
|
|
111
|
-
|
|
112
|
-
On-premises SystemLink Server (SLS) instances may use custom domains
|
|
113
|
-
or even *.systemlink.io subdomains (like base.systemlink.io).
|
|
114
|
-
|
|
115
|
-
Args:
|
|
116
|
-
api_url: The SystemLink API base URL
|
|
117
|
-
|
|
118
|
-
Returns:
|
|
119
|
-
Platform identifier: PLATFORM_SLE or PLATFORM_SLS.
|
|
120
|
-
Note: This function never returns PLATFORM_UNKNOWN - it defaults to SLS.
|
|
121
|
-
"""
|
|
122
|
-
api_url_lower = api_url.lower()
|
|
123
|
-
|
|
124
|
-
# SLE cloud service has specific URL patterns
|
|
125
|
-
sle_patterns = [
|
|
126
|
-
"api.systemlink.io", # SLE production
|
|
127
|
-
"-api.lifecyclesolutions.ni.com", # SLE dev/demo with -api suffix
|
|
128
|
-
"dev-api.lifecyclesolutions",
|
|
129
|
-
"demo-api.lifecyclesolutions",
|
|
130
|
-
]
|
|
131
|
-
for pattern in sle_patterns:
|
|
132
|
-
if pattern in api_url_lower:
|
|
133
|
-
return PLATFORM_SLE
|
|
134
|
-
|
|
135
|
-
# Default to SLS for on-premises/custom URLs
|
|
136
|
-
# This includes on-prem servers that may use *.systemlink.io subdomains
|
|
137
|
-
return PLATFORM_SLS
|
|
138
|
-
|
|
139
|
-
|
|
140
103
|
@lru_cache(maxsize=1)
|
|
141
104
|
def get_platform() -> str:
|
|
142
105
|
"""Get the current platform from stored configuration or environment.
|
|
@@ -144,8 +107,7 @@ def get_platform() -> str:
|
|
|
144
107
|
Detection priority:
|
|
145
108
|
1. SYSTEMLINK_PLATFORM environment variable (explicit, most reliable)
|
|
146
109
|
2. Stored platform from keyring config (set during login via endpoint probing)
|
|
147
|
-
3.
|
|
148
|
-
4. Return PLATFORM_UNKNOWN if all methods fail
|
|
110
|
+
3. Return PLATFORM_UNKNOWN if no explicit or stored platform is available
|
|
149
111
|
|
|
150
112
|
Note: Results are cached for performance. Use clear_platform_cache() to reset.
|
|
151
113
|
|
|
@@ -166,12 +128,6 @@ def get_platform() -> str:
|
|
|
166
128
|
if platform in (PLATFORM_SLE, PLATFORM_SLS):
|
|
167
129
|
return platform
|
|
168
130
|
|
|
169
|
-
# Priority 3: URL pattern matching (fallback, less reliable)
|
|
170
|
-
# Only used when env vars are set but no explicit platform is provided
|
|
171
|
-
env_url = os.environ.get("SYSTEMLINK_API_URL")
|
|
172
|
-
if env_url:
|
|
173
|
-
return _detect_platform_from_url(env_url)
|
|
174
|
-
|
|
175
131
|
return PLATFORM_UNKNOWN
|
|
176
132
|
|
|
177
133
|
|
|
@@ -240,7 +196,7 @@ SERVICE_CHECKS: List[List[str]] = [
|
|
|
240
196
|
["Asset Management", "POST", "/niapm/v1/query-assets"],
|
|
241
197
|
["Systems", "POST", "/nisysmgmt/v1/query-systems"],
|
|
242
198
|
["Tag", "GET", "/nitag/v2/tags?take=0"],
|
|
243
|
-
["File", "POST",
|
|
199
|
+
["File", "POST", FILE_SEARCH_PATH],
|
|
244
200
|
["Notebook", "POST", "/ninotebook/v1/notebook/query"],
|
|
245
201
|
["Web Application", "POST", "/niapp/v1/webapps/query"],
|
|
246
202
|
["Dynamic Form Fields", "GET", "/nidynamicformfields/v1/groups"],
|
|
@@ -248,6 +204,87 @@ SERVICE_CHECKS: List[List[str]] = [
|
|
|
248
204
|
]
|
|
249
205
|
|
|
250
206
|
|
|
207
|
+
def get_file_query_capability(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
208
|
+
"""Determine which file query endpoint is available for this server."""
|
|
209
|
+
headers = {
|
|
210
|
+
"x-ni-api-key": api_key,
|
|
211
|
+
"Content-Type": "application/json",
|
|
212
|
+
"User-Agent": "SystemLink-CLI/1.0 (cross-platform)",
|
|
213
|
+
}
|
|
214
|
+
ssl_verify = get_ssl_verify()
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
search_resp = requests.post(
|
|
218
|
+
f"{api_url}{FILE_SEARCH_PATH}",
|
|
219
|
+
headers=headers,
|
|
220
|
+
json={"take": 1},
|
|
221
|
+
verify=ssl_verify,
|
|
222
|
+
timeout=10,
|
|
223
|
+
)
|
|
224
|
+
if search_resp.status_code in (200, 400):
|
|
225
|
+
return {
|
|
226
|
+
"status": "ok",
|
|
227
|
+
"file_query_endpoint": "search-files",
|
|
228
|
+
"elasticsearch_available": True,
|
|
229
|
+
}
|
|
230
|
+
if search_resp.status_code in (401, 403):
|
|
231
|
+
return {
|
|
232
|
+
"status": "unauthorized",
|
|
233
|
+
"file_query_endpoint": "search-files",
|
|
234
|
+
"elasticsearch_available": True,
|
|
235
|
+
}
|
|
236
|
+
if search_resp.status_code not in (404, 501):
|
|
237
|
+
return {
|
|
238
|
+
"status": "error" if search_resp.status_code >= 500 else "not_found",
|
|
239
|
+
"file_query_endpoint": None,
|
|
240
|
+
"elasticsearch_available": True,
|
|
241
|
+
}
|
|
242
|
+
except requests.RequestException:
|
|
243
|
+
return {
|
|
244
|
+
"status": "unreachable",
|
|
245
|
+
"file_query_endpoint": None,
|
|
246
|
+
"elasticsearch_available": None,
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
fallback_resp = requests.post(
|
|
251
|
+
f"{api_url}{FILE_QUERY_LINQ_PATH}",
|
|
252
|
+
headers=headers,
|
|
253
|
+
json={"take": 1},
|
|
254
|
+
verify=ssl_verify,
|
|
255
|
+
timeout=10,
|
|
256
|
+
)
|
|
257
|
+
if fallback_resp.status_code in (200, 400):
|
|
258
|
+
return {
|
|
259
|
+
"status": "fallback",
|
|
260
|
+
"file_query_endpoint": "query-files-linq",
|
|
261
|
+
"elasticsearch_available": False,
|
|
262
|
+
}
|
|
263
|
+
if fallback_resp.status_code in (401, 403):
|
|
264
|
+
return {
|
|
265
|
+
"status": "unauthorized",
|
|
266
|
+
"file_query_endpoint": "query-files-linq",
|
|
267
|
+
"elasticsearch_available": False,
|
|
268
|
+
}
|
|
269
|
+
if fallback_resp.status_code in (404, 501):
|
|
270
|
+
return {
|
|
271
|
+
"status": "not_found",
|
|
272
|
+
"file_query_endpoint": None,
|
|
273
|
+
"elasticsearch_available": False,
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
"status": "error",
|
|
277
|
+
"file_query_endpoint": None,
|
|
278
|
+
"elasticsearch_available": False,
|
|
279
|
+
}
|
|
280
|
+
except requests.RequestException:
|
|
281
|
+
return {
|
|
282
|
+
"status": "unreachable",
|
|
283
|
+
"file_query_endpoint": None,
|
|
284
|
+
"elasticsearch_available": False,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
251
288
|
def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
252
289
|
"""Probe key SystemLink services and report their status.
|
|
253
290
|
|
|
@@ -263,8 +300,10 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
263
300
|
- auth_valid: bool | None - whether the API key is authorized (None if unreachable)
|
|
264
301
|
- services: dict mapping service name to status string
|
|
265
302
|
("ok", "unauthorized", "not_found", "error", "unreachable")
|
|
266
|
-
|
|
267
|
-
|
|
303
|
+
- file_query_endpoint: selected file query endpoint, if available
|
|
304
|
+
- elasticsearch_available: bool | None - whether search-files is available
|
|
305
|
+
- platform: detected platform string (PLATFORM_SLE, PLATFORM_SLS,
|
|
306
|
+
PLATFORM_UNREACHABLE, PLATFORM_UNKNOWN)
|
|
268
307
|
"""
|
|
269
308
|
headers = {
|
|
270
309
|
"x-ni-api-key": api_key,
|
|
@@ -321,6 +360,8 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
321
360
|
"server_reachable": False,
|
|
322
361
|
"auth_valid": None,
|
|
323
362
|
"services": services,
|
|
363
|
+
"file_query_endpoint": None,
|
|
364
|
+
"elasticsearch_available": None,
|
|
324
365
|
"platform": PLATFORM_UNREACHABLE,
|
|
325
366
|
}
|
|
326
367
|
|
|
@@ -337,13 +378,17 @@ def check_service_status(api_url: str, api_key: str) -> Dict[str, Any]:
|
|
|
337
378
|
elif workorder_status == "not_found":
|
|
338
379
|
platform = PLATFORM_SLS
|
|
339
380
|
else:
|
|
340
|
-
|
|
341
|
-
|
|
381
|
+
platform = PLATFORM_UNKNOWN
|
|
382
|
+
|
|
383
|
+
file_capability = get_file_query_capability(api_url, api_key)
|
|
384
|
+
services["File"] = file_capability["status"]
|
|
342
385
|
|
|
343
386
|
return {
|
|
344
387
|
"server_reachable": True,
|
|
345
388
|
"auth_valid": auth_valid,
|
|
346
389
|
"services": services,
|
|
390
|
+
"file_query_endpoint": file_capability["file_query_endpoint"],
|
|
391
|
+
"elasticsearch_available": file_capability["elasticsearch_available"],
|
|
347
392
|
"platform": platform,
|
|
348
393
|
}
|
|
349
394
|
|
|
@@ -392,6 +437,8 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
|
392
437
|
auth_valid: Optional[bool] = None
|
|
393
438
|
services: Optional[Dict[str, str]] = None
|
|
394
439
|
platform = stored_platform
|
|
440
|
+
file_query_endpoint: Optional[str] = None
|
|
441
|
+
elasticsearch_available: Optional[bool] = None
|
|
395
442
|
|
|
396
443
|
if not skip_health and logged_in and isinstance(api_url, str) and api_url != "Not configured":
|
|
397
444
|
status = check_service_status(api_url, api_key)
|
|
@@ -399,6 +446,8 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
|
399
446
|
auth_valid = status["auth_valid"]
|
|
400
447
|
services = status["services"]
|
|
401
448
|
platform = status["platform"]
|
|
449
|
+
file_query_endpoint = status.get("file_query_endpoint")
|
|
450
|
+
elasticsearch_available = status.get("elasticsearch_available")
|
|
402
451
|
|
|
403
452
|
info: Dict[str, Any] = {
|
|
404
453
|
"api_url": api_url,
|
|
@@ -410,6 +459,11 @@ def get_platform_info(skip_health: bool = False) -> Dict[str, Any]:
|
|
|
410
459
|
"auth_valid": auth_valid,
|
|
411
460
|
}
|
|
412
461
|
|
|
462
|
+
if file_query_endpoint is not None:
|
|
463
|
+
info["file_query_endpoint"] = file_query_endpoint
|
|
464
|
+
if elasticsearch_available is not None:
|
|
465
|
+
info["elasticsearch_available"] = elasticsearch_available
|
|
466
|
+
|
|
413
467
|
if services is not None:
|
|
414
468
|
info["services"] = services
|
|
415
469
|
|
|
@@ -423,7 +423,7 @@ def _query_all_users(
|
|
|
423
423
|
- Uses 'status = "active"' for filtering disabled users
|
|
424
424
|
|
|
425
425
|
TODO: Follow this pattern for other API clients that support continuation tokens
|
|
426
|
-
Reference: https://
|
|
426
|
+
Reference: https://demo-api.lifecyclesolutions.ni.com/niuser/swagger/v1/niuser.yaml
|
|
427
427
|
|
|
428
428
|
Args:
|
|
429
429
|
filter_str: Filter expression for users
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/demo-complete-workflow/config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/exercise-7-1-test-plans/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/exercise-7-1-test-plans/config.yaml
RENAMED
|
File without changes
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/examples/spec-compliance-notebooks/config.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.4.2 → systemlink_cli-1.4.4}/slcli/skills/slcli/references/analysis-recipes.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|