systemlink-cli 1.13.3__tar.gz → 1.13.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.13.3 → systemlink_cli-1.13.4}/PKG-INFO +1 -1
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/dff-editor/editor.js +96 -21
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/pyproject.toml +1 -1
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/_version.py +1 -1
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/notebook_click.py +60 -5
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/web_editor.py +123 -16
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/LICENSE +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/dff-editor/index.html +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/__init__.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/__main__.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/asset_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/cli_formatters.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/cli_utils.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/comment_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/completion_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/config.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/config_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/dataframe_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/dff_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/dff_decorators.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/example_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/example_loader.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/example_provisioner.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/README.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/_schema/schema-v1.0.json +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/demo-complete-workflow/README.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/demo-complete-workflow/config.yaml +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/demo-test-plans/README.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/demo-test-plans/config.yaml +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/exercise-5-1-parametric-insights/README.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/exercise-5-1-parametric-insights/config.yaml +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/exercise-7-1-test-plans/README.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/exercise-7-1-test-plans/config.yaml +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/README.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/config.yaml +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/feed_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/file_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/function_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/function_templates.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/main.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/mcp_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/mcp_reachability.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/mcp_server.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/platform.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/policy_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/policy_utils.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/profiles.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/response_handlers.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/rich_output.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/routine_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skill_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/nipkg-file-package/SKILL.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/SKILL.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/analysis-recipes.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/commands.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/datasheet-workflow.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/filtering.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/troubleshooting.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-job-debugging/SKILL.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-notebook/SKILL.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-notebook/references/interfaces.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-notebook/references/notebook-patterns.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-python-test/SKILL.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-webapp/SKILL.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-webapp/references/deployment.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-webapp/references/layout-patterns.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-webapp/references/nimble-angular.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-webapp/references/systemlink-services.md +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/spec_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/ssl_trust.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/state_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/system_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/system_query_utils.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/table_utils.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/tag_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/templates_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/testmonitor_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/universal_handlers.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/user_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/utils.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/webapp_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/workflow_preview.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/workflows_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/workitem_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/workspace_click.py +0 -0
- {systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/workspace_utils.py +0 -0
|
@@ -651,9 +651,7 @@ function downloadConfiguration() {
|
|
|
651
651
|
const a = document.createElement('a');
|
|
652
652
|
a.href = url;
|
|
653
653
|
a.download = 'dff-configuration.json';
|
|
654
|
-
document.body.appendChild(a);
|
|
655
654
|
a.click();
|
|
656
|
-
document.body.removeChild(a);
|
|
657
655
|
URL.revokeObjectURL(url);
|
|
658
656
|
showStatus('Configuration downloaded', 'success');
|
|
659
657
|
} catch (e) {
|
|
@@ -682,53 +680,130 @@ function refreshTree() {
|
|
|
682
680
|
|
|
683
681
|
const config = currentConfig;
|
|
684
682
|
if (!config) return;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
683
|
+
|
|
684
|
+
treeView.replaceChildren();
|
|
685
|
+
treeView.appendChild(createTreeNode({
|
|
686
|
+
nodeId: 'root',
|
|
687
|
+
icon: '📄',
|
|
688
|
+
label: 'Root Configuration'
|
|
689
|
+
}));
|
|
690
|
+
|
|
689
691
|
if (config.configurations && config.configurations.length > 0) {
|
|
690
692
|
config.configurations.forEach((conf, i) => {
|
|
691
693
|
const confLabel = conf.displayText || conf.name || conf.key || ('Configuration ' + (i + 1));
|
|
692
|
-
|
|
693
|
-
|
|
694
|
+
treeView.appendChild(createTreeNode({
|
|
695
|
+
nodeId: `config-${i}`,
|
|
696
|
+
icon: '⚙️',
|
|
697
|
+
label: confLabel,
|
|
698
|
+
indent: 1,
|
|
699
|
+
editLabel: `Edit configuration: ${confLabel}`,
|
|
700
|
+
editTitle: 'Edit configuration',
|
|
701
|
+
onEdit: () => showEditDialog('configuration', i)
|
|
702
|
+
}));
|
|
703
|
+
|
|
694
704
|
if (conf.views && conf.views.length > 0) {
|
|
695
705
|
conf.views.forEach((view, vi) => {
|
|
696
|
-
|
|
706
|
+
const viewLabel = view.displayText || view.key;
|
|
707
|
+
treeView.appendChild(createTreeNode({
|
|
708
|
+
nodeId: `config-${i}-view-${vi}`,
|
|
709
|
+
icon: '👁️',
|
|
710
|
+
label: viewLabel,
|
|
711
|
+
indent: 2,
|
|
712
|
+
editLabel: `Edit view: ${viewLabel}`,
|
|
713
|
+
editTitle: 'Edit view',
|
|
714
|
+
onEdit: () => showEditDialog('view', i, vi)
|
|
715
|
+
}));
|
|
697
716
|
});
|
|
698
717
|
}
|
|
699
718
|
});
|
|
700
719
|
}
|
|
701
|
-
|
|
702
|
-
// Groups
|
|
720
|
+
|
|
703
721
|
if (config.groups && config.groups.length > 0) {
|
|
704
|
-
|
|
722
|
+
treeView.appendChild(createTreeSummaryNode('📁', `Groups (${config.groups.length})`, 1));
|
|
705
723
|
config.groups.forEach((group, i) => {
|
|
706
724
|
const groupLabel = group.displayText || group.key;
|
|
707
|
-
|
|
725
|
+
treeView.appendChild(createTreeNode({
|
|
726
|
+
nodeId: `group-${i}`,
|
|
727
|
+
icon: '📦',
|
|
728
|
+
label: groupLabel,
|
|
729
|
+
indent: 2,
|
|
730
|
+
editLabel: `Edit group: ${groupLabel}`,
|
|
731
|
+
editTitle: 'Edit group',
|
|
732
|
+
onEdit: () => showEditDialog('group', i)
|
|
733
|
+
}));
|
|
708
734
|
});
|
|
709
735
|
}
|
|
710
|
-
|
|
711
|
-
// Fields
|
|
736
|
+
|
|
712
737
|
if (config.fields && config.fields.length > 0) {
|
|
713
|
-
|
|
738
|
+
treeView.appendChild(createTreeSummaryNode('📁', `Fields (${config.fields.length})`, 1));
|
|
714
739
|
config.fields.forEach((field, i) => {
|
|
715
740
|
const icon = field.required ? '🏷️' : '🔖';
|
|
716
741
|
const fieldLabel = field.displayText || field.key;
|
|
717
|
-
|
|
742
|
+
treeView.appendChild(createTreeNode({
|
|
743
|
+
nodeId: `field-${i}`,
|
|
744
|
+
icon,
|
|
745
|
+
label: fieldLabel,
|
|
746
|
+
indent: 2,
|
|
747
|
+
editLabel: `Edit field: ${fieldLabel}`,
|
|
748
|
+
editTitle: 'Edit field',
|
|
749
|
+
onEdit: () => showEditDialog('field', i)
|
|
750
|
+
}));
|
|
718
751
|
});
|
|
719
752
|
}
|
|
720
|
-
|
|
721
|
-
treeView.innerHTML = html;
|
|
722
753
|
}
|
|
723
754
|
|
|
724
|
-
function
|
|
755
|
+
function createTreeSummaryNode(icon, label, indent = 0) {
|
|
756
|
+
const node = document.createElement('div');
|
|
757
|
+
node.className = 'tree-node';
|
|
758
|
+
if (indent > 0) {
|
|
759
|
+
node.classList.add(`indent-${indent}`);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const iconElement = document.createElement('span');
|
|
763
|
+
iconElement.className = 'tree-icon';
|
|
764
|
+
iconElement.textContent = icon;
|
|
765
|
+
node.appendChild(iconElement);
|
|
766
|
+
|
|
767
|
+
const labelElement = document.createElement('span');
|
|
768
|
+
labelElement.textContent = label;
|
|
769
|
+
node.appendChild(labelElement);
|
|
770
|
+
|
|
771
|
+
return node;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function createTreeNode({ nodeId, icon, label, indent = 0, editLabel = '', editTitle = '', onEdit = null }) {
|
|
775
|
+
const node = createTreeSummaryNode(icon, label, indent);
|
|
776
|
+
node.dataset.nodeId = nodeId;
|
|
777
|
+
node.addEventListener('click', () => selectTreeNode(nodeId, node));
|
|
778
|
+
|
|
779
|
+
if (onEdit) {
|
|
780
|
+
const editButton = document.createElement('button');
|
|
781
|
+
editButton.className = 'edit-btn';
|
|
782
|
+
editButton.type = 'button';
|
|
783
|
+
editButton.textContent = '✎';
|
|
784
|
+
editButton.setAttribute('aria-label', editLabel);
|
|
785
|
+
editButton.title = editTitle;
|
|
786
|
+
editButton.addEventListener('click', (event) => {
|
|
787
|
+
event.stopPropagation();
|
|
788
|
+
onEdit();
|
|
789
|
+
});
|
|
790
|
+
node.appendChild(editButton);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return node;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function selectTreeNode(nodeId, nodeElement = null) {
|
|
725
797
|
selectedTreeNode = nodeId;
|
|
726
798
|
|
|
727
799
|
// Update visual selection
|
|
728
800
|
document.querySelectorAll('.tree-node').forEach(node => {
|
|
729
801
|
node.classList.remove('selected');
|
|
730
802
|
});
|
|
731
|
-
|
|
803
|
+
const activeNode = nodeElement || document.querySelector(`[data-node-id="${nodeId}"]`);
|
|
804
|
+
if (activeNode) {
|
|
805
|
+
activeNode.classList.add('selected');
|
|
806
|
+
}
|
|
732
807
|
|
|
733
808
|
// Highlight corresponding JSON in editor
|
|
734
809
|
selectNodeInEditor(nodeId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "systemlink-cli"
|
|
3
|
-
version = "1.13.
|
|
3
|
+
version = "1.13.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" }]
|
|
@@ -6,6 +6,7 @@ All commands use Click for robust CLI interfaces and error handling.
|
|
|
6
6
|
|
|
7
7
|
import datetime
|
|
8
8
|
import json
|
|
9
|
+
import re
|
|
9
10
|
import sys
|
|
10
11
|
import time
|
|
11
12
|
import urllib.parse
|
|
@@ -50,6 +51,56 @@ PREDEFINED_NOTEBOOK_INTERFACES = [
|
|
|
50
51
|
"Work Item Scheduler",
|
|
51
52
|
]
|
|
52
53
|
|
|
54
|
+
WINDOWS_RESERVED_FILENAMES = {
|
|
55
|
+
"CON",
|
|
56
|
+
"PRN",
|
|
57
|
+
"AUX",
|
|
58
|
+
"NUL",
|
|
59
|
+
"COM1",
|
|
60
|
+
"COM2",
|
|
61
|
+
"COM3",
|
|
62
|
+
"COM4",
|
|
63
|
+
"COM5",
|
|
64
|
+
"COM6",
|
|
65
|
+
"COM7",
|
|
66
|
+
"COM8",
|
|
67
|
+
"COM9",
|
|
68
|
+
"LPT1",
|
|
69
|
+
"LPT2",
|
|
70
|
+
"LPT3",
|
|
71
|
+
"LPT4",
|
|
72
|
+
"LPT5",
|
|
73
|
+
"LPT6",
|
|
74
|
+
"LPT7",
|
|
75
|
+
"LPT8",
|
|
76
|
+
"LPT9",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_safe_notebook_download_name(notebook_name: str, suffix: str) -> str:
|
|
81
|
+
"""Build a local filename from remote notebook metadata without honoring path segments."""
|
|
82
|
+
name_segment = notebook_name.replace("\\", "/").split("/")[-1].strip()
|
|
83
|
+
if name_segment in {"", ".", ".."}:
|
|
84
|
+
name_segment = "notebook"
|
|
85
|
+
|
|
86
|
+
stem = name_segment[:-6] if name_segment.lower().endswith(".ipynb") else name_segment
|
|
87
|
+
safe_stem = re.sub(r"[^A-Za-z0-9._ -]+", "_", stem).strip(" ._") or "notebook"
|
|
88
|
+
if safe_stem.upper() in WINDOWS_RESERVED_FILENAMES:
|
|
89
|
+
safe_stem = f"{safe_stem}_file"
|
|
90
|
+
normalized_suffix = suffix if suffix.startswith(".") else f".{suffix}"
|
|
91
|
+
|
|
92
|
+
return f"{safe_stem}{normalized_suffix}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _get_safe_notebook_download_path(notebook_name: str, suffix: str) -> Path:
|
|
96
|
+
"""Build a safe local output path for remote-derived notebook filenames."""
|
|
97
|
+
safe_filename = _get_safe_notebook_download_name(notebook_name, suffix)
|
|
98
|
+
working_directory = Path.cwd().resolve()
|
|
99
|
+
output_path = (working_directory / safe_filename).resolve()
|
|
100
|
+
if output_path.parent != working_directory:
|
|
101
|
+
raise ValueError("Notebook download path must stay within the current working directory")
|
|
102
|
+
return output_path
|
|
103
|
+
|
|
53
104
|
|
|
54
105
|
def _normalize_sls_notebook(notebook: Dict[str, Any]) -> None:
|
|
55
106
|
"""Normalize SLS notebook response to include id/name fields.
|
|
@@ -529,9 +580,10 @@ def _download_notebook_content_and_metadata(
|
|
|
529
580
|
if download_type in ("content", "both"):
|
|
530
581
|
try:
|
|
531
582
|
content = _get_notebook_content_http(notebook_id)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
583
|
+
if output:
|
|
584
|
+
output_path = Path(output)
|
|
585
|
+
else:
|
|
586
|
+
output_path = _get_safe_notebook_download_path(notebook_name, ".ipynb")
|
|
535
587
|
with open(output_path, "wb") as f:
|
|
536
588
|
f.write(content)
|
|
537
589
|
click.echo(f"Notebook content downloaded to {output_path}")
|
|
@@ -543,14 +595,17 @@ def _download_notebook_content_and_metadata(
|
|
|
543
595
|
if download_type in ("metadata", "both"):
|
|
544
596
|
try:
|
|
545
597
|
meta = _get_notebook_http(notebook_id)
|
|
546
|
-
|
|
598
|
+
if output:
|
|
599
|
+
meta_path = Path(f"{output}.json")
|
|
600
|
+
else:
|
|
601
|
+
meta_path = _get_safe_notebook_download_path(notebook_name, ".json")
|
|
547
602
|
|
|
548
603
|
def _json_default(obj: Any) -> str:
|
|
549
604
|
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
550
605
|
return obj.isoformat()
|
|
551
606
|
return str(obj)
|
|
552
607
|
|
|
553
|
-
save_json_file(meta, meta_path, _json_default)
|
|
608
|
+
save_json_file(meta, str(meta_path), _json_default)
|
|
554
609
|
click.echo(f"Notebook metadata downloaded to {meta_path}")
|
|
555
610
|
except Exception as exc:
|
|
556
611
|
click.echo(f"Failed to download notebook metadata: {exc}")
|
|
@@ -43,10 +43,9 @@ def _build_proxy_url(
|
|
|
43
43
|
origin_scheme: str,
|
|
44
44
|
origin_netloc: str,
|
|
45
45
|
target_path: str,
|
|
46
|
-
query: str = "",
|
|
47
46
|
) -> str:
|
|
48
47
|
"""Build a proxy URL from a validated origin and allowlisted path."""
|
|
49
|
-
return urllib.parse.urlunsplit((origin_scheme, origin_netloc, target_path,
|
|
48
|
+
return urllib.parse.urlunsplit((origin_scheme, origin_netloc, target_path, "", ""))
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
def _validated_proxy_path(request_path: str) -> str:
|
|
@@ -59,6 +58,118 @@ def _validated_proxy_path(request_path: str) -> str:
|
|
|
59
58
|
return decoded_path
|
|
60
59
|
|
|
61
60
|
|
|
61
|
+
def _validated_proxy_query_params(query: str) -> dict[str, list[str]]:
|
|
62
|
+
"""Return parsed proxy query parameters."""
|
|
63
|
+
if not query:
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
return urllib.parse.parse_qs(
|
|
68
|
+
query,
|
|
69
|
+
keep_blank_values=True,
|
|
70
|
+
strict_parsing=True,
|
|
71
|
+
separator="&",
|
|
72
|
+
)
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
raise ValueError("Editor proxy received an invalid query string") from exc
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _validated_single_query_value(
|
|
78
|
+
query_params: dict[str, list[str]],
|
|
79
|
+
name: str,
|
|
80
|
+
) -> str:
|
|
81
|
+
"""Return a single query value and reject repeated parameters."""
|
|
82
|
+
values = query_params.get(name)
|
|
83
|
+
if not values:
|
|
84
|
+
raise ValueError(f"Editor proxy requires query parameter: {name}")
|
|
85
|
+
if len(values) != 1:
|
|
86
|
+
raise ValueError(f"Editor proxy rejects repeated query parameter: {name}")
|
|
87
|
+
return values[0]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _validated_integer_query_value(
|
|
91
|
+
query_params: dict[str, list[str]],
|
|
92
|
+
name: str,
|
|
93
|
+
*,
|
|
94
|
+
minimum: int,
|
|
95
|
+
maximum: Optional[int] = None,
|
|
96
|
+
) -> str:
|
|
97
|
+
"""Return a validated integer query value preserved as a string."""
|
|
98
|
+
raw_value = _validated_single_query_value(query_params, name)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
parsed_value = int(raw_value)
|
|
102
|
+
except ValueError as exc:
|
|
103
|
+
raise ValueError(f"Editor proxy requires an integer query parameter: {name}") from exc
|
|
104
|
+
|
|
105
|
+
if parsed_value < minimum or (maximum is not None and parsed_value > maximum):
|
|
106
|
+
raise ValueError(f"Editor proxy rejected out-of-range query parameter: {name}")
|
|
107
|
+
|
|
108
|
+
return str(parsed_value)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _validated_identifier_query_value(query_params: dict[str, list[str]], name: str) -> str:
|
|
112
|
+
"""Return a single identifier-like query value safe for proxy forwarding."""
|
|
113
|
+
value = _validated_single_query_value(query_params, name)
|
|
114
|
+
if not value.strip():
|
|
115
|
+
raise ValueError(f"Editor proxy requires a non-empty query parameter: {name}")
|
|
116
|
+
if len(value) > 256:
|
|
117
|
+
raise ValueError(f"Editor proxy rejected oversized query parameter: {name}")
|
|
118
|
+
if any(ord(character) < 32 for character in value):
|
|
119
|
+
raise ValueError(f"Editor proxy rejected invalid query parameter: {name}")
|
|
120
|
+
if any(character in value for character in "/\\?#"):
|
|
121
|
+
raise ValueError(f"Editor proxy rejected invalid query parameter: {name}")
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _resolve_proxy_target(
|
|
126
|
+
method: str,
|
|
127
|
+
request_path: str,
|
|
128
|
+
query: str,
|
|
129
|
+
) -> Optional[tuple[str, dict[str, str]]]:
|
|
130
|
+
"""Resolve a frontend route to a fixed upstream path and validated params."""
|
|
131
|
+
query_params = _validated_proxy_query_params(query)
|
|
132
|
+
|
|
133
|
+
if method == "POST":
|
|
134
|
+
post_route_map = {
|
|
135
|
+
"/api/dff/configurations": "/nidynamicformfields/v1/configurations",
|
|
136
|
+
"/api/dff/update-configurations": "/nidynamicformfields/v1/update-configurations",
|
|
137
|
+
"/nidynamicformfields/v1/update-configurations": "/nidynamicformfields/v1/update-configurations",
|
|
138
|
+
}
|
|
139
|
+
target_path = post_route_map.get(request_path)
|
|
140
|
+
if target_path is None:
|
|
141
|
+
return None
|
|
142
|
+
if query_params:
|
|
143
|
+
raise ValueError("Editor proxy does not forward query parameters for this route")
|
|
144
|
+
return target_path, {}
|
|
145
|
+
|
|
146
|
+
if method == "GET" and request_path == "/niuser/v1/workspaces":
|
|
147
|
+
unexpected_params = set(query_params) - {"take", "skip"}
|
|
148
|
+
if unexpected_params:
|
|
149
|
+
raise ValueError("Editor proxy rejected unsupported workspace query parameters")
|
|
150
|
+
|
|
151
|
+
safe_params: dict[str, str] = {}
|
|
152
|
+
if "take" in query_params:
|
|
153
|
+
safe_params["take"] = _validated_integer_query_value(
|
|
154
|
+
query_params, "take", minimum=1, maximum=1000
|
|
155
|
+
)
|
|
156
|
+
if "skip" in query_params:
|
|
157
|
+
safe_params["skip"] = _validated_integer_query_value(query_params, "skip", minimum=0)
|
|
158
|
+
return "/niuser/v1/workspaces", safe_params
|
|
159
|
+
|
|
160
|
+
if method == "GET" and request_path == "/nidynamicformfields/v1/resolved-configuration":
|
|
161
|
+
unexpected_params = set(query_params) - {"configurationId"}
|
|
162
|
+
if unexpected_params:
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"Editor proxy rejected unsupported resolved-configuration query parameters"
|
|
165
|
+
)
|
|
166
|
+
return "/nidynamicformfields/v1/resolved-configuration", {
|
|
167
|
+
"configurationId": _validated_identifier_query_value(query_params, "configurationId")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
62
173
|
class DFFWebEditor:
|
|
63
174
|
"""Web-based editor for custom fields configurations."""
|
|
64
175
|
|
|
@@ -224,21 +335,17 @@ class DFFWebEditor:
|
|
|
224
335
|
self.send_error(404, "Config file not found")
|
|
225
336
|
return True
|
|
226
337
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if
|
|
234
|
-
target_path = path_map[request_path]
|
|
235
|
-
elif request_path.startswith("/nidynamicformfields/v1/"):
|
|
236
|
-
target_path = request_path
|
|
237
|
-
elif request_path.startswith("/niuser/v1/workspaces"):
|
|
238
|
-
target_path = request_path
|
|
239
|
-
else:
|
|
338
|
+
try:
|
|
339
|
+
resolved_target = _resolve_proxy_target(method, request_path, parsed.query)
|
|
340
|
+
except ValueError as exc:
|
|
341
|
+
self.send_error(400, str(exc))
|
|
342
|
+
return True
|
|
343
|
+
|
|
344
|
+
if resolved_target is None:
|
|
240
345
|
return False
|
|
241
346
|
|
|
347
|
+
target_path, target_params = resolved_target
|
|
348
|
+
|
|
242
349
|
# Require per-session secret on all proxied routes
|
|
243
350
|
req_secret = self.headers.get("X-Editor-Secret")
|
|
244
351
|
if not secret or req_secret != secret:
|
|
@@ -249,7 +356,6 @@ class DFFWebEditor:
|
|
|
249
356
|
origin_scheme=api_scheme,
|
|
250
357
|
origin_netloc=api_netloc,
|
|
251
358
|
target_path=target_path,
|
|
252
|
-
query=parsed.query,
|
|
253
359
|
)
|
|
254
360
|
|
|
255
361
|
headers = dict(default_headers)
|
|
@@ -266,6 +372,7 @@ class DFFWebEditor:
|
|
|
266
372
|
url=target_url,
|
|
267
373
|
headers=headers,
|
|
268
374
|
data=data,
|
|
375
|
+
params=target_params or None,
|
|
269
376
|
verify=ssl_verify,
|
|
270
377
|
)
|
|
271
378
|
except requests.RequestException as exc: # pragma: no cover
|
|
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
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/demo-complete-workflow/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.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.13.3 → systemlink_cli-1.13.4}/slcli/examples/exercise-7-1-test-plans/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/exercise-7-1-test-plans/config.yaml
RENAMED
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/examples/spec-compliance-notebooks/README.md
RENAMED
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/analysis-recipes.md
RENAMED
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/datasheet-workflow.md
RENAMED
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/slcli/references/troubleshooting.md
RENAMED
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-job-debugging/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{systemlink_cli-1.13.3 → systemlink_cli-1.13.4}/slcli/skills/systemlink-python-test/SKILL.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|