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.
fraclab_sdk/version.py ADDED
@@ -0,0 +1,5 @@
1
+ """Package version metadata."""
2
+
3
+ __version__ = "0.1.2"
4
+
5
+ __all__ = ["__version__"]
@@ -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
- (tmp_dir_path / "drs.json").write_text(json.dumps({"datasets": []}, indent=2))
466
- (tmp_dir_path / "params.schema.json").write_text(
467
- json.dumps({"type": "object", "properties": {}}, indent=2)
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
- st.success("Validation Passed!", icon="✅")
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
- for issue in result.issues:
147
- st.warning(f"[{issue.severity}] {issue.code}: {issue.message} ({getattr(issue, 'path', '')})")
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 / "dist" / "output_contract.json")
135
+
136
+ result = validate_output_contract(workspace_dir)
137
137
  if result.valid:
138
- st.success("Validation Passed!", icon="✅")
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
- for issue in result.issues:
142
- st.warning(f"[{issue.severity}] {issue.code}: {issue.message}")
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. Package Integrity Check & DRS Source
88
+ # 2. Validation Status
83
89
  # ==========================================
84
- st.subheader("2. Package Integrity Check")
85
-
86
- def _get_status_html(path: Path, label: str):
87
- exists = path.exists()
88
- status_cls = "status-ok" if exists else "status-missing"
89
- icon = "✅" if exists else "❌"
90
- text = "Present" if exists else "Missing"
91
- return f"""
92
- <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f2f6;">
93
- <span style="font-weight: 500;">{label}</span>
94
- <span class="status-badge {status_cls}">{icon} {text}</span>
95
- </div>
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
- col_health, col_preview = st.columns([1, 2])
100
-
101
- with col_health:
102
- st.markdown("#### File Status")
103
- st.markdown(_get_status_html(manifest_path, "Manifest"), unsafe_allow_html=True)
104
- st.markdown(_get_status_html(params_schema_path, "Input Schema"), unsafe_allow_html=True)
105
- st.markdown(_get_status_html(output_contract_path, "Output Contract"), unsafe_allow_html=True)
106
-
107
- # DRS: Show prompt instead of file check
108
- st.markdown("""
109
- <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f2f6;">
110
- <span style="font-weight: 500;">DRS</span>
111
- <span style="color: #6b7280; font-size: 0.85rem;">👇 Select below</span>
112
- </div>
113
- """, unsafe_allow_html=True)
114
-
115
- # Manifest Metadata Preview
116
- if manifest_path.exists():
117
- st.markdown("---")
118
- try:
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.info("File not generated yet.")
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(base_name=tmpdir_path / "algorithm_export", format="zip", root_dir=tmpdir_path, base_dir=algo_dir.name)
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
- _, col_btn = st.columns([3, 1])
224
- with col_btn:
225
- if st.button("📦 Build & Export", type="primary", width="stretch"):
226
- try:
227
- with st.spinner("Packaging..."):
228
- zip_bytes = build_zip()
229
-
230
- st.download_button(
231
- label="⬇️ Download Zip",
232
- data=zip_bytes,
233
- file_name=f"{selected_algo.algorithm_id}-{selected_algo.version}.zip",
234
- mime="application/zip",
235
- width="stretch"
236
- )
237
- except Exception as e:
238
- st.error(f"Export failed: {e}")
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 hamburger menu and Deploy button */
20
- [data-testid="stToolbar"], .stDeployButton {
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.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>/` 目录结构: