fraclab-sdk 0.1.1__py3-none-any.whl → 0.1.2__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.
- CHANGELOG.md +50 -0
- README.md +2 -0
- fraclab_sdk/__init__.py +3 -0
- fraclab_sdk/devkit/__init__.py +8 -0
- fraclab_sdk/devkit/validate.py +836 -75
- fraclab_sdk/version.py +5 -0
- fraclab_sdk/workbench/pages/1_Snapshots.py +35 -4
- fraclab_sdk/workbench/pages/2_Browse.py +1 -1
- fraclab_sdk/workbench/pages/3_Selection.py +1 -1
- fraclab_sdk/workbench/pages/4_Run.py +7 -1
- fraclab_sdk/workbench/pages/5_Results.py +7 -1
- fraclab_sdk/workbench/pages/6_Algorithm_Edit.py +1 -1
- fraclab_sdk/workbench/pages/7_Schema_Edit.py +17 -6
- fraclab_sdk/workbench/pages/8_Output_Edit.py +18 -7
- fraclab_sdk/workbench/pages/9_Export_Algorithm.py +232 -84
- fraclab_sdk/workbench/ui_styles.py +14 -2
- {fraclab_sdk-0.1.1.dist-info → fraclab_sdk-0.1.2.dist-info}/METADATA +3 -1
- {fraclab_sdk-0.1.1.dist-info → fraclab_sdk-0.1.2.dist-info}/RECORD +20 -18
- {fraclab_sdk-0.1.1.dist-info → fraclab_sdk-0.1.2.dist-info}/WHEEL +0 -0
- {fraclab_sdk-0.1.1.dist-info → fraclab_sdk-0.1.2.dist-info}/entry_points.txt +0 -0
fraclab_sdk/version.py
ADDED
|
@@ -12,10 +12,11 @@ from fraclab_sdk.algorithm import AlgorithmLibrary
|
|
|
12
12
|
from fraclab_sdk.config import SDKConfig
|
|
13
13
|
from fraclab_sdk.errors import SnapshotError
|
|
14
14
|
from fraclab_sdk.snapshot import SnapshotLibrary
|
|
15
|
+
from fraclab_sdk.version import __version__ as SDK_VERSION
|
|
15
16
|
from fraclab_sdk.workbench import ui_styles
|
|
16
17
|
from fraclab_sdk.workbench.utils import get_workspace_dir
|
|
17
18
|
|
|
18
|
-
st.set_page_config(page_title="Snapshots", page_icon="📦", layout="wide")
|
|
19
|
+
st.set_page_config(page_title="Snapshots", page_icon="📦", layout="wide", initial_sidebar_state="expanded")
|
|
19
20
|
st.title("Snapshots")
|
|
20
21
|
|
|
21
22
|
ui_styles.apply_global_styles()
|
|
@@ -123,6 +124,10 @@ def create_algorithm_scaffold(
|
|
|
123
124
|
"drsPath": "dist/drs.json",
|
|
124
125
|
"outputContractPath": "dist/output_contract.json",
|
|
125
126
|
},
|
|
127
|
+
"requires": {"sdk": SDK_VERSION},
|
|
128
|
+
"repository": None,
|
|
129
|
+
"homepage": None,
|
|
130
|
+
"license": None,
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
(ws_dir / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
@@ -134,6 +139,10 @@ def create_algorithm_scaffold(
|
|
|
134
139
|
json.dumps({"type": "object", "title": "Parameters", "properties": {}}, indent=2),
|
|
135
140
|
encoding="utf-8",
|
|
136
141
|
)
|
|
142
|
+
(dist_dir / "output_contract.json").write_text(
|
|
143
|
+
json.dumps({"datasets": [], "invariants": [], "relations": []}, indent=2),
|
|
144
|
+
encoding="utf-8",
|
|
145
|
+
)
|
|
137
146
|
|
|
138
147
|
main_stub = '''"""Algorithm entrypoint."""
|
|
139
148
|
|
|
@@ -459,13 +468,29 @@ with st.expander("📤 Import Existing Algorithm", expanded=True):
|
|
|
459
468
|
try:
|
|
460
469
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
461
470
|
tmp_dir_path = Path(tmp_dir)
|
|
471
|
+
|
|
472
|
+
# Copy uploaded main.py
|
|
462
473
|
algo_path = tmp_dir_path / "main.py"
|
|
463
474
|
algo_path.write_bytes(uploaded_algorithm.getvalue())
|
|
464
475
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
476
|
+
# Create dist/ with template files
|
|
477
|
+
dist_dir = tmp_dir_path / "dist"
|
|
478
|
+
dist_dir.mkdir(parents=True, exist_ok=True)
|
|
479
|
+
(dist_dir / "drs.json").write_text(json.dumps({"datasets": []}, indent=2))
|
|
480
|
+
(dist_dir / "params.schema.json").write_text(
|
|
481
|
+
json.dumps({"type": "object", "title": "Parameters", "properties": {}}, indent=2)
|
|
468
482
|
)
|
|
483
|
+
(dist_dir / "output_contract.json").write_text(
|
|
484
|
+
json.dumps({"datasets": [], "invariants": [], "relations": []}, indent=2)
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Create schema/ with base utilities
|
|
488
|
+
schema_dir = tmp_dir_path / "schema"
|
|
489
|
+
schema_dir.mkdir(parents=True, exist_ok=True)
|
|
490
|
+
(schema_dir / "__init__.py").write_text("", encoding="utf-8")
|
|
491
|
+
(schema_dir / "base.py").write_text(BASE_SCHEMA_UTILS, encoding="utf-8")
|
|
492
|
+
|
|
493
|
+
# Create manifest
|
|
469
494
|
algo_id = uploaded_algorithm.name.removesuffix(".py")
|
|
470
495
|
manifest = {
|
|
471
496
|
"manifestVersion": "1",
|
|
@@ -475,6 +500,12 @@ with st.expander("📤 Import Existing Algorithm", expanded=True):
|
|
|
475
500
|
"name": algo_id,
|
|
476
501
|
"summary": "Imported from single python file",
|
|
477
502
|
"authors": [{"name": "unknown"}],
|
|
503
|
+
"files": {
|
|
504
|
+
"paramsSchemaPath": "dist/params.schema.json",
|
|
505
|
+
"drsPath": "dist/drs.json",
|
|
506
|
+
"outputContractPath": "dist/output_contract.json",
|
|
507
|
+
},
|
|
508
|
+
"requires": {"sdk": SDK_VERSION},
|
|
478
509
|
}
|
|
479
510
|
(tmp_dir_path / "manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
480
511
|
|
|
@@ -10,7 +10,7 @@ from fraclab_sdk.config import SDKConfig
|
|
|
10
10
|
from fraclab_sdk.snapshot import SnapshotLibrary
|
|
11
11
|
from fraclab_sdk.workbench import ui_styles
|
|
12
12
|
|
|
13
|
-
st.set_page_config(page_title="Browse", page_icon="🔍", layout="wide")
|
|
13
|
+
st.set_page_config(page_title="Browse", page_icon="🔍", layout="wide", initial_sidebar_state="expanded")
|
|
14
14
|
st.title("Browse")
|
|
15
15
|
|
|
16
16
|
ui_styles.apply_global_styles()
|
|
@@ -13,7 +13,7 @@ from fraclab_sdk.selection.model import SelectionModel
|
|
|
13
13
|
from fraclab_sdk.snapshot import SnapshotLibrary
|
|
14
14
|
from fraclab_sdk.workbench import ui_styles
|
|
15
15
|
|
|
16
|
-
st.set_page_config(page_title="Selection", page_icon="✅", layout="wide")
|
|
16
|
+
st.set_page_config(page_title="Selection", page_icon="✅", layout="wide", initial_sidebar_state="expanded")
|
|
17
17
|
st.title("Selection")
|
|
18
18
|
|
|
19
19
|
ui_styles.apply_global_styles()
|
|
@@ -11,11 +11,17 @@ from fraclab_sdk.config import SDKConfig
|
|
|
11
11
|
from fraclab_sdk.run import RunManager, RunStatus
|
|
12
12
|
from fraclab_sdk.workbench import ui_styles
|
|
13
13
|
|
|
14
|
-
st.set_page_config(page_title="Run", page_icon="▶️", layout="wide")
|
|
14
|
+
st.set_page_config(page_title="Run", page_icon="▶️", layout="wide", initial_sidebar_state="expanded")
|
|
15
15
|
st.title("Run")
|
|
16
16
|
|
|
17
17
|
ui_styles.apply_global_styles()
|
|
18
18
|
|
|
19
|
+
# Guidance: if no params UI shows up, validate InputSpec via the editor page.
|
|
20
|
+
st.info(
|
|
21
|
+
"看不到参数输入框?请在 Schema Editor 页面点击 Validate 检查 "
|
|
22
|
+
"`schema/inputspec.py`,确保生成的 schema 可用。",
|
|
23
|
+
icon="ℹ️",
|
|
24
|
+
)
|
|
19
25
|
# --- Page-Specific CSS ---
|
|
20
26
|
st.markdown("""
|
|
21
27
|
<style>
|
|
@@ -17,11 +17,17 @@ from fraclab_sdk.results import (
|
|
|
17
17
|
from fraclab_sdk.run import RunManager, RunStatus
|
|
18
18
|
from fraclab_sdk.workbench import ui_styles
|
|
19
19
|
|
|
20
|
-
st.set_page_config(page_title="Results", page_icon="📊", layout="wide")
|
|
20
|
+
st.set_page_config(page_title="Results", page_icon="📊", layout="wide", initial_sidebar_state="expanded")
|
|
21
21
|
st.title("Results")
|
|
22
22
|
|
|
23
23
|
ui_styles.apply_global_styles()
|
|
24
24
|
|
|
25
|
+
# Guidance: if artifacts preview looks empty, validate OutputContract via the editor page.
|
|
26
|
+
st.info(
|
|
27
|
+
"看不到期望的输出?请在 Output Editor 页面点击 Validate 检查 "
|
|
28
|
+
"`schema/output_contract.py` 的 datasets/items/artifacts 是否正确。",
|
|
29
|
+
icon="ℹ️",
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
def get_manager():
|
|
27
33
|
"""Get run manager."""
|
|
@@ -9,7 +9,7 @@ from fraclab_sdk.algorithm import AlgorithmLibrary
|
|
|
9
9
|
from fraclab_sdk.config import SDKConfig
|
|
10
10
|
from fraclab_sdk.workbench import ui_styles
|
|
11
11
|
|
|
12
|
-
st.set_page_config(page_title="Algorithm Editor", page_icon="✏️", layout="wide")
|
|
12
|
+
st.set_page_config(page_title="Algorithm Editor", page_icon="✏️", layout="wide", initial_sidebar_state="expanded")
|
|
13
13
|
st.title("Algorithm Editor")
|
|
14
14
|
|
|
15
15
|
ui_styles.apply_global_styles()
|
|
@@ -13,7 +13,7 @@ from fraclab_sdk.devkit.validate import validate_inputspec
|
|
|
13
13
|
from fraclab_sdk.workbench import ui_styles
|
|
14
14
|
from fraclab_sdk.workbench.utils import run_workspace_script
|
|
15
15
|
|
|
16
|
-
st.set_page_config(page_title="Schema Editor", page_icon="🧩", layout="wide")
|
|
16
|
+
st.set_page_config(page_title="Schema Editor", page_icon="🧩", layout="wide", initial_sidebar_state="expanded")
|
|
17
17
|
st.title("Schema Editor (InputSpec)")
|
|
18
18
|
|
|
19
19
|
ui_styles.apply_global_styles()
|
|
@@ -137,13 +137,24 @@ with col_valid:
|
|
|
137
137
|
# Auto-save before validate
|
|
138
138
|
inputspec_path.write_text(edited, encoding="utf-8")
|
|
139
139
|
write_params_schema(algo_dir)
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
result = validate_inputspec(algo_dir)
|
|
142
142
|
if result.valid:
|
|
143
|
-
|
|
143
|
+
if result.warnings:
|
|
144
|
+
st.warning(f"Validation Passed with {len(result.warnings)} warning(s)", icon="⚠️")
|
|
145
|
+
else:
|
|
146
|
+
st.success("Validation Passed!", icon="✅")
|
|
144
147
|
else:
|
|
145
|
-
st.error("Validation Failed", icon="🚫")
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
st.error(f"Validation Failed ({len(result.errors)} error(s))", icon="🚫")
|
|
149
|
+
|
|
150
|
+
# Show all issues (errors and warnings)
|
|
151
|
+
for issue in result.issues:
|
|
152
|
+
icon = "🔴" if issue.severity.value == "error" else "🟡"
|
|
153
|
+
path_str = f" at `{issue.path}`" if issue.path else ""
|
|
154
|
+
details_str = ""
|
|
155
|
+
if issue.details:
|
|
156
|
+
if "suggested" in issue.details:
|
|
157
|
+
details_str = f" → Suggested: `{issue.details['suggested']}`"
|
|
158
|
+
st.markdown(f"{icon} **{issue.code}**{path_str}: {issue.message}{details_str}")
|
|
148
159
|
except Exception as e:
|
|
149
160
|
st.error(f"Validation error: {e}")
|
|
@@ -14,7 +14,7 @@ from fraclab_sdk.devkit.validate import validate_output_contract
|
|
|
14
14
|
from fraclab_sdk.workbench import ui_styles
|
|
15
15
|
from fraclab_sdk.workbench.utils import run_workspace_script
|
|
16
16
|
|
|
17
|
-
st.set_page_config(page_title="OutputSpec Editor", page_icon="📤", layout="wide")
|
|
17
|
+
st.set_page_config(page_title="OutputSpec Editor", page_icon="📤", layout="wide", initial_sidebar_state="expanded")
|
|
18
18
|
st.title("OutputSpec Editor")
|
|
19
19
|
|
|
20
20
|
ui_styles.apply_global_styles()
|
|
@@ -132,13 +132,24 @@ with col_valid:
|
|
|
132
132
|
# Auto-save before validate
|
|
133
133
|
output_spec_path.write_text(edited, encoding="utf-8")
|
|
134
134
|
write_dist_from_contract(workspace_dir, selected.algorithm_id, selected.version)
|
|
135
|
-
|
|
136
|
-
result = validate_output_contract(workspace_dir
|
|
135
|
+
|
|
136
|
+
result = validate_output_contract(workspace_dir)
|
|
137
137
|
if result.valid:
|
|
138
|
-
|
|
138
|
+
if result.warnings:
|
|
139
|
+
st.warning(f"Validation Passed with {len(result.warnings)} warning(s)", icon="⚠️")
|
|
140
|
+
else:
|
|
141
|
+
st.success("Validation Passed!", icon="✅")
|
|
139
142
|
else:
|
|
140
|
-
st.error("Validation Failed", icon="🚫")
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
st.error(f"Validation Failed ({len(result.errors)} error(s))", icon="🚫")
|
|
144
|
+
|
|
145
|
+
# Show all issues (errors and warnings)
|
|
146
|
+
for issue in result.issues:
|
|
147
|
+
icon = "🔴" if issue.severity.value == "error" else "🟡"
|
|
148
|
+
path_str = f" at `{issue.path}`" if issue.path else ""
|
|
149
|
+
details_str = ""
|
|
150
|
+
if issue.details:
|
|
151
|
+
if "suggested" in issue.details:
|
|
152
|
+
details_str = f" → Suggested: `{issue.details['suggested']}`"
|
|
153
|
+
st.markdown(f"{icon} **{issue.code}**{path_str}: {issue.message}{details_str}")
|
|
143
154
|
except Exception as e:
|
|
144
155
|
st.error(f"Validation error: {e}")
|
|
@@ -12,10 +12,15 @@ import streamlit as st
|
|
|
12
12
|
|
|
13
13
|
from fraclab_sdk.algorithm import AlgorithmLibrary
|
|
14
14
|
from fraclab_sdk.config import SDKConfig
|
|
15
|
+
from fraclab_sdk.devkit import (
|
|
16
|
+
validate_algorithm_signature,
|
|
17
|
+
validate_inputspec,
|
|
18
|
+
validate_output_contract,
|
|
19
|
+
)
|
|
15
20
|
from fraclab_sdk.snapshot import SnapshotLibrary
|
|
16
21
|
from fraclab_sdk.workbench import ui_styles
|
|
17
22
|
|
|
18
|
-
st.set_page_config(page_title="Export Algorithm", page_icon="📦", layout="wide")
|
|
23
|
+
st.set_page_config(page_title="Export Algorithm", page_icon="📦", layout="wide", initial_sidebar_state="expanded")
|
|
19
24
|
st.title("Export Algorithm")
|
|
20
25
|
|
|
21
26
|
ui_styles.apply_global_styles()
|
|
@@ -32,6 +37,7 @@ st.markdown("""
|
|
|
32
37
|
}
|
|
33
38
|
.status-ok { background-color: #d1fae5; color: #065f46; }
|
|
34
39
|
.status-missing { background-color: #fee2e2; color: #991b1b; }
|
|
40
|
+
.status-warning { background-color: #fef3c7; color: #92400e; }
|
|
35
41
|
</style>
|
|
36
42
|
""", unsafe_allow_html=True)
|
|
37
43
|
|
|
@@ -79,67 +85,197 @@ output_contract_path = algo_dir / "dist" / "output_contract.json"
|
|
|
79
85
|
drs_path = algo_dir / "dist" / "drs.json"
|
|
80
86
|
|
|
81
87
|
# ==========================================
|
|
82
|
-
# 2.
|
|
88
|
+
# 2. Validation Status
|
|
83
89
|
# ==========================================
|
|
84
|
-
st.subheader("2.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
st.subheader("2. Validation Status")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_algo_mtime(algo_dir: Path) -> float:
|
|
94
|
+
"""Get max mtime of source files for cache invalidation."""
|
|
95
|
+
files = [
|
|
96
|
+
algo_dir / "main.py",
|
|
97
|
+
algo_dir / "manifest.json",
|
|
98
|
+
]
|
|
99
|
+
# schema/*.py files
|
|
100
|
+
schema_dir = algo_dir / "schema"
|
|
101
|
+
if schema_dir.exists():
|
|
102
|
+
files.extend(schema_dir.glob("*.py"))
|
|
103
|
+
|
|
104
|
+
# dist/*.json (if exists)
|
|
105
|
+
dist_dir = algo_dir / "dist"
|
|
106
|
+
if dist_dir.exists():
|
|
107
|
+
files.extend(dist_dir.glob("*.json"))
|
|
108
|
+
|
|
109
|
+
mtimes = [f.stat().st_mtime for f in files if f.exists()]
|
|
110
|
+
return max(mtimes) if mtimes else 0.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@st.cache_data(ttl=60)
|
|
114
|
+
def _run_all_validations(algo_dir_str: str, _mtime: float) -> dict[str, dict]:
|
|
115
|
+
"""Run all validations. Cached by (path, mtime)."""
|
|
116
|
+
workspace = Path(algo_dir_str)
|
|
117
|
+
results: dict[str, dict] = {}
|
|
118
|
+
|
|
119
|
+
# InputSpec validation
|
|
120
|
+
try:
|
|
121
|
+
inputspec_result = validate_inputspec(workspace)
|
|
122
|
+
results["inputspec"] = {
|
|
123
|
+
"valid": inputspec_result.valid,
|
|
124
|
+
"errors": len(inputspec_result.errors),
|
|
125
|
+
"warnings": len(inputspec_result.warnings),
|
|
126
|
+
"issues": [
|
|
127
|
+
{
|
|
128
|
+
"severity": i.severity.value,
|
|
129
|
+
"code": i.code,
|
|
130
|
+
"message": i.message,
|
|
131
|
+
"path": i.path,
|
|
132
|
+
"details": i.details,
|
|
133
|
+
}
|
|
134
|
+
for i in inputspec_result.issues
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
except Exception as e:
|
|
138
|
+
results["inputspec"] = {
|
|
139
|
+
"valid": False,
|
|
140
|
+
"errors": 1,
|
|
141
|
+
"warnings": 0,
|
|
142
|
+
"issues": [{"severity": "error", "code": "VALIDATION_FAILED", "message": str(e), "path": None, "details": {}}],
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# OutputContract validation
|
|
146
|
+
try:
|
|
147
|
+
output_result = validate_output_contract(workspace)
|
|
148
|
+
results["output_contract"] = {
|
|
149
|
+
"valid": output_result.valid,
|
|
150
|
+
"errors": len(output_result.errors),
|
|
151
|
+
"warnings": len(output_result.warnings),
|
|
152
|
+
"issues": [
|
|
153
|
+
{
|
|
154
|
+
"severity": i.severity.value,
|
|
155
|
+
"code": i.code,
|
|
156
|
+
"message": i.message,
|
|
157
|
+
"path": i.path,
|
|
158
|
+
"details": i.details,
|
|
159
|
+
}
|
|
160
|
+
for i in output_result.issues
|
|
161
|
+
],
|
|
162
|
+
}
|
|
163
|
+
except Exception as e:
|
|
164
|
+
results["output_contract"] = {
|
|
165
|
+
"valid": False,
|
|
166
|
+
"errors": 1,
|
|
167
|
+
"warnings": 0,
|
|
168
|
+
"issues": [{"severity": "error", "code": "VALIDATION_FAILED", "message": str(e), "path": None, "details": {}}],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# Algorithm signature validation
|
|
172
|
+
try:
|
|
173
|
+
algo_result = validate_algorithm_signature(workspace)
|
|
174
|
+
results["algorithm"] = {
|
|
175
|
+
"valid": algo_result.valid,
|
|
176
|
+
"errors": len(algo_result.errors),
|
|
177
|
+
"warnings": len(algo_result.warnings),
|
|
178
|
+
"issues": [
|
|
179
|
+
{
|
|
180
|
+
"severity": i.severity.value,
|
|
181
|
+
"code": i.code,
|
|
182
|
+
"message": i.message,
|
|
183
|
+
"path": i.path,
|
|
184
|
+
"details": i.details,
|
|
185
|
+
}
|
|
186
|
+
for i in algo_result.issues
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
except Exception as e:
|
|
190
|
+
results["algorithm"] = {
|
|
191
|
+
"valid": False,
|
|
192
|
+
"errors": 1,
|
|
193
|
+
"warnings": 0,
|
|
194
|
+
"issues": [{"severity": "error", "code": "VALIDATION_FAILED", "message": str(e), "path": None, "details": {}}],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return results
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# Run validations with caching
|
|
201
|
+
mtime = _get_algo_mtime(algo_dir)
|
|
202
|
+
validation_results = _run_all_validations(str(algo_dir), mtime)
|
|
203
|
+
|
|
204
|
+
# Display validation status badges with revalidate button
|
|
98
205
|
with st.container(border=True):
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
m_data = json.loads(manifest_path.read_text())
|
|
120
|
-
st.caption("Manifest Metadata")
|
|
121
|
-
st.text(f"Code Ver: {m_data.get('codeVersion')}")
|
|
122
|
-
st.text(f"Contract: {m_data.get('contractVersion')}")
|
|
123
|
-
except:
|
|
124
|
-
st.error("Invalid Manifest")
|
|
125
|
-
|
|
126
|
-
with col_preview:
|
|
127
|
-
st.markdown("#### File Inspector")
|
|
128
|
-
tab_man, tab_in, tab_out = st.tabs(["Manifest", "Input Spec", "Output Spec"])
|
|
129
|
-
|
|
130
|
-
def _show_json_preview(path: Path):
|
|
131
|
-
if path.exists():
|
|
132
|
-
try:
|
|
133
|
-
data = json.loads(path.read_text())
|
|
134
|
-
st.code(json.dumps(data, indent=2, ensure_ascii=False), language="json", line_numbers=True)
|
|
135
|
-
except Exception:
|
|
136
|
-
st.error("Failed to parse JSON")
|
|
206
|
+
cols = st.columns([1, 1, 1, 0.5])
|
|
207
|
+
names = ["InputSpec", "OutputContract", "Algorithm"]
|
|
208
|
+
keys = ["inputspec", "output_contract", "algorithm"]
|
|
209
|
+
|
|
210
|
+
for i, (key, name) in enumerate(zip(keys, names)):
|
|
211
|
+
result = validation_results.get(key, {})
|
|
212
|
+
error_count = result.get("errors", 0)
|
|
213
|
+
warning_count = result.get("warnings", 0)
|
|
214
|
+
|
|
215
|
+
with cols[i]:
|
|
216
|
+
if error_count > 0:
|
|
217
|
+
st.markdown(
|
|
218
|
+
f'<span class="status-badge status-missing">❌ {name}: {error_count} error{"s" if error_count > 1 else ""}</span>',
|
|
219
|
+
unsafe_allow_html=True,
|
|
220
|
+
)
|
|
221
|
+
elif warning_count > 0:
|
|
222
|
+
st.markdown(
|
|
223
|
+
f'<span class="status-badge status-warning">⚠️ {name}: {warning_count} warning{"s" if warning_count > 1 else ""}</span>',
|
|
224
|
+
unsafe_allow_html=True,
|
|
225
|
+
)
|
|
137
226
|
else:
|
|
138
|
-
st.
|
|
227
|
+
st.markdown(
|
|
228
|
+
f'<span class="status-badge status-ok">✅ {name}</span>',
|
|
229
|
+
unsafe_allow_html=True,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
with cols[3]:
|
|
233
|
+
if st.button("🔄 Revalidate", key="rerun_validation"):
|
|
234
|
+
_run_all_validations.clear()
|
|
235
|
+
st.rerun()
|
|
236
|
+
|
|
237
|
+
# Collect all issues
|
|
238
|
+
all_issues = []
|
|
239
|
+
for key in keys:
|
|
240
|
+
result = validation_results.get(key, {})
|
|
241
|
+
for issue in result.get("issues", []):
|
|
242
|
+
all_issues.append((key, issue))
|
|
243
|
+
|
|
244
|
+
# Show validation details if there are issues
|
|
245
|
+
if all_issues:
|
|
246
|
+
with st.expander(f"📋 Validation Details ({len(all_issues)} issue{'s' if len(all_issues) > 1 else ''})", expanded=False):
|
|
247
|
+
for source, issue in all_issues:
|
|
248
|
+
icon = {"error": "🔴", "warning": "🟡", "info": "🔵"}.get(issue["severity"], "⚪")
|
|
249
|
+
path_str = f" at `{issue['path']}`" if issue.get("path") else ""
|
|
250
|
+
details_str = ""
|
|
251
|
+
if issue.get("details"):
|
|
252
|
+
# Show suggested fix for snake_case issues
|
|
253
|
+
if "suggested" in issue["details"]:
|
|
254
|
+
details_str = f" → Suggested: `{issue['details']['suggested']}`"
|
|
255
|
+
elif "missing" in issue["details"]:
|
|
256
|
+
details_str = f" (missing: {issue['details']['missing']})"
|
|
257
|
+
elif "extra" in issue["details"]:
|
|
258
|
+
details_str = f" (extra: {issue['details']['extra']})"
|
|
259
|
+
st.markdown(f"{icon} **[{source}]** `{issue['code']}`{path_str}: {issue['message']}{details_str}")
|
|
260
|
+
|
|
261
|
+
# File Inspector
|
|
262
|
+
with st.expander("📂 File Inspector", expanded=True):
|
|
263
|
+
tab_man, tab_in, tab_out = st.tabs(["Manifest", "Input Spec", "Output Spec"])
|
|
264
|
+
|
|
265
|
+
def _show_json_preview(path: Path):
|
|
266
|
+
if path.exists():
|
|
267
|
+
try:
|
|
268
|
+
data = json.loads(path.read_text())
|
|
269
|
+
st.code(json.dumps(data, indent=2, ensure_ascii=False), language="json", line_numbers=True)
|
|
270
|
+
except Exception:
|
|
271
|
+
st.error("Failed to parse JSON")
|
|
272
|
+
else:
|
|
273
|
+
st.info("File not generated yet.")
|
|
274
|
+
|
|
275
|
+
with tab_man: _show_json_preview(manifest_path)
|
|
276
|
+
with tab_in: _show_json_preview(params_schema_path)
|
|
277
|
+
with tab_out: _show_json_preview(output_contract_path)
|
|
139
278
|
|
|
140
|
-
with tab_man: _show_json_preview(manifest_path)
|
|
141
|
-
with tab_in: _show_json_preview(params_schema_path)
|
|
142
|
-
with tab_out: _show_json_preview(output_contract_path)
|
|
143
279
|
|
|
144
280
|
# ==========================================
|
|
145
281
|
# 3. DRS Source Selection
|
|
@@ -180,59 +316,71 @@ def build_zip() -> bytes:
|
|
|
180
316
|
# copy installed algorithm content
|
|
181
317
|
shutil.copytree(algo_dir, tmpdir_path / algo_dir.name, dirs_exist_ok=True)
|
|
182
318
|
target_root = tmpdir_path / algo_dir.name
|
|
183
|
-
|
|
319
|
+
|
|
184
320
|
# ensure manifest files paths cover dist outputs if present
|
|
185
321
|
manifest_data = json.loads(manifest_path.read_text())
|
|
186
322
|
files = manifest_data.get("files") or {}
|
|
187
|
-
|
|
323
|
+
|
|
188
324
|
if output_contract_path.exists():
|
|
189
325
|
files["outputContractPath"] = "dist/output_contract.json"
|
|
190
326
|
if params_schema_path.exists():
|
|
191
327
|
files["paramsSchemaPath"] = "dist/params.schema.json"
|
|
192
328
|
if drs_path.exists():
|
|
193
329
|
files["drsPath"] = "dist/drs.json"
|
|
194
|
-
|
|
330
|
+
|
|
195
331
|
if files:
|
|
196
332
|
manifest_data["files"] = files
|
|
197
|
-
|
|
333
|
+
|
|
198
334
|
(target_root / "manifest.json").write_text(json.dumps(manifest_data, indent=2), encoding="utf-8")
|
|
199
|
-
|
|
335
|
+
|
|
200
336
|
# DRS Override Logic
|
|
201
337
|
# Try to find DRS path from manifest, default to dist/drs.json
|
|
202
338
|
drs_rel_path = manifest_data.get("files", {}).get("drsPath", "dist/drs.json")
|
|
203
339
|
target_drs_path = target_root / drs_rel_path
|
|
204
340
|
target_drs_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
-
|
|
341
|
+
|
|
206
342
|
# Read DRS from Snapshot
|
|
207
343
|
snap_drs_path = snapshot_handle.directory / snapshot_handle.manifest.specFiles.drsPath
|
|
208
|
-
|
|
344
|
+
|
|
209
345
|
if snap_drs_path.exists():
|
|
210
346
|
target_drs_path.write_bytes(snap_drs_path.read_bytes())
|
|
211
347
|
else:
|
|
212
348
|
# Fallback if snapshot DRS is missing structure (rare)
|
|
213
|
-
pass
|
|
349
|
+
pass
|
|
214
350
|
|
|
215
|
-
# Zip it up
|
|
351
|
+
# Zip it up (flattened: no top-level version folder)
|
|
216
352
|
zip_buf = io.BytesIO()
|
|
217
|
-
shutil.make_archive(
|
|
353
|
+
shutil.make_archive(
|
|
354
|
+
base_name=tmpdir_path / "algorithm_export",
|
|
355
|
+
format="zip",
|
|
356
|
+
root_dir=target_root,
|
|
357
|
+
base_dir=".",
|
|
358
|
+
)
|
|
218
359
|
zip_path = tmpdir_path / "algorithm_export.zip"
|
|
219
360
|
zip_buf.write(zip_path.read_bytes())
|
|
220
361
|
zip_buf.seek(0)
|
|
221
362
|
return zip_buf.read()
|
|
222
363
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
364
|
+
|
|
365
|
+
# Check if there are validation errors
|
|
366
|
+
has_validation_errors = any(not r.get("valid", True) for r in validation_results.values())
|
|
367
|
+
|
|
368
|
+
_, col_export_btn = st.columns([3, 1])
|
|
369
|
+
with col_export_btn:
|
|
370
|
+
if has_validation_errors:
|
|
371
|
+
st.error("Fix validation errors to export")
|
|
372
|
+
st.button("📦 Build & Export", type="primary", disabled=True, key="export_disabled")
|
|
373
|
+
else:
|
|
374
|
+
if st.button("📦 Build & Export", type="primary", key="export_enabled"):
|
|
375
|
+
try:
|
|
376
|
+
with st.spinner("Packaging..."):
|
|
377
|
+
zip_bytes = build_zip()
|
|
378
|
+
|
|
379
|
+
st.download_button(
|
|
380
|
+
label="⬇️ Download Zip",
|
|
381
|
+
data=zip_bytes,
|
|
382
|
+
file_name=f"{selected_algo.algorithm_id}-{selected_algo.version}.zip",
|
|
383
|
+
mime="application/zip",
|
|
384
|
+
)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
st.error(f"Export failed: {e}")
|
|
@@ -16,8 +16,20 @@ def apply_global_styles():
|
|
|
16
16
|
display: none !important;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/* Hide
|
|
20
|
-
|
|
19
|
+
/* Hide Deploy button; keep toolbar for sidebar toggle */
|
|
20
|
+
.stDeployButton {
|
|
21
|
+
display: none !important;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Force sidebar visible and disable collapse control */
|
|
25
|
+
[data-testid="stSidebar"] {
|
|
26
|
+
transform: none !important;
|
|
27
|
+
min-width: 260px;
|
|
28
|
+
}
|
|
29
|
+
[data-testid="stSidebarNav"] {
|
|
30
|
+
min-width: 240px;
|
|
31
|
+
}
|
|
32
|
+
[data-testid="collapsedControl"] {
|
|
21
33
|
display: none !important;
|
|
22
34
|
}
|
|
23
35
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fraclab-sdk
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: SDK for managing snapshots, algorithms, and run execution
|
|
5
5
|
Requires-Python: >=3.11,<4.0
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -913,6 +913,8 @@ $ fraclab-sdk run tail f9e8d7c6
|
|
|
913
913
|
$ fraclab-sdk run tail f9e8d7c6 --stderr
|
|
914
914
|
```
|
|
915
915
|
|
|
916
|
+
> Workbench 提示:结果页面会展示本次运行的输出目录路径(含 `_logs` 日志),即使运行失败也能点开路径定位调试。
|
|
917
|
+
|
|
916
918
|
#### 6. 运行目录结构
|
|
917
919
|
|
|
918
920
|
执行完成后,`~/.fraclab/runs/<run_id>/` 目录结构:
|