fraclab-sdk 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,116 @@
1
+ """Algorithm editor page."""
2
+
3
+ import json
4
+ import shutil
5
+
6
+ import streamlit as st
7
+
8
+ from fraclab_sdk.algorithm import AlgorithmLibrary
9
+ from fraclab_sdk.config import SDKConfig
10
+ from fraclab_sdk.workbench import ui_styles
11
+
12
+ st.set_page_config(page_title="Algorithm Editor", page_icon="✏️", layout="wide")
13
+ st.title("Algorithm Editor")
14
+
15
+ ui_styles.apply_global_styles()
16
+
17
+ # --- Page-Specific CSS: Editor Styling ---
18
+ st.markdown("""
19
+ <style>
20
+ /* Make Text Area look like a code editor */
21
+ textarea {
22
+ font-family: "Source Code Pro", "Consolas", "Courier New", monospace !important;
23
+ font-size: 14px !important;
24
+ line-height: 1.5 !important;
25
+ color: #333 !important;
26
+ background-color: #fcfcfc !important;
27
+ }
28
+ </style>
29
+ """, unsafe_allow_html=True)
30
+
31
+
32
+ config = SDKConfig()
33
+ algo_lib = AlgorithmLibrary(config)
34
+ algos = algo_lib.list_algorithms()
35
+
36
+ if not algos:
37
+ st.info("No algorithms imported. Use the Snapshots page to import one.")
38
+ st.stop()
39
+
40
+ # --- 1. Selection Bar ---
41
+ with st.container(border=True):
42
+ c1, c2 = st.columns([3, 1])
43
+ with c1:
44
+ algo_options = {f"{a.algorithm_id}:{a.version}": a for a in algos}
45
+ selected_key = st.selectbox(
46
+ "Select Algorithm",
47
+ options=list(algo_options.keys()),
48
+ format_func=lambda k: f"{algo_options[k].algorithm_id} (v{algo_options[k].version})",
49
+ label_visibility="collapsed"
50
+ )
51
+ with c2:
52
+ if selected_key:
53
+ selected = algo_options[selected_key]
54
+ st.caption(f"ID: `{selected.algorithm_id}`")
55
+
56
+ if not selected_key:
57
+ st.stop()
58
+
59
+ # Load Data
60
+ handle = algo_lib.get_algorithm(selected.algorithm_id, selected.version)
61
+ algo_dir = handle.directory
62
+ algo_file = algo_dir / "main.py"
63
+ manifest_file = algo_dir / "manifest.json"
64
+
65
+ algo_text = algo_file.read_text(encoding="utf-8") if algo_file.exists() else ""
66
+ manifest = json.loads(manifest_file.read_text(encoding="utf-8")) if manifest_file.exists() else {}
67
+ current_version = manifest.get("codeVersion", selected.version)
68
+
69
+ # --- 2. Action Bar (Specific to Algorithm Edit: Versioning) ---
70
+ col_ver, col_spacer, col_save = st.columns([2, 4, 1])
71
+
72
+ with col_ver:
73
+ new_version = st.text_input("Target Version", value=current_version, help="Change this to save as a new version")
74
+
75
+ # --- 3. Editor Area ---
76
+ st.caption(f"Editing: `{algo_file}`")
77
+ edited_text = st.text_area(
78
+ "Code Editor",
79
+ value=algo_text,
80
+ height=600,
81
+ label_visibility="collapsed"
82
+ )
83
+
84
+ # --- Save Logic ---
85
+ with col_save:
86
+ # Button aligned with the input box visually
87
+ st.write("")
88
+ st.write("")
89
+ if st.button("💾 Save Changes", type="primary", width="stretch"):
90
+ try:
91
+ # Write edits to current workspace
92
+ algo_dir.mkdir(parents=True, exist_ok=True)
93
+ algo_file.write_text(edited_text, encoding="utf-8")
94
+
95
+ # Update manifest version and write
96
+ if manifest:
97
+ manifest["codeVersion"] = new_version
98
+ manifest_file.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
99
+
100
+ # If version changed, copy to new workspace folder
101
+ if new_version != selected.version:
102
+ new_dir = config.algorithms_dir / selected.algorithm_id / new_version
103
+ new_dir.mkdir(parents=True, exist_ok=True)
104
+ shutil.copytree(algo_dir, new_dir, dirs_exist_ok=True)
105
+ # Ensure manifest in new dir reflects version
106
+ new_manifest_path = new_dir / "manifest.json"
107
+ if new_manifest_path.exists():
108
+ new_manifest = json.loads(new_manifest_path.read_text())
109
+ new_manifest["codeVersion"] = new_version
110
+ new_manifest_path.write_text(json.dumps(new_manifest, indent=2), encoding="utf-8")
111
+ st.toast(f"Saved as new version: {new_version}", icon="✅")
112
+ st.success(f"New workspace created at: `{new_dir}`")
113
+ else:
114
+ st.toast("File saved successfully", icon="✅")
115
+ except Exception as e:
116
+ st.error(f"Save failed: {e}")
@@ -0,0 +1,149 @@
1
+ """Schema editor page for editing algorithm InputSpec."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import streamlit as st
9
+
10
+ from fraclab_sdk.algorithm import AlgorithmLibrary
11
+ from fraclab_sdk.config import SDKConfig
12
+ from fraclab_sdk.devkit.validate import validate_inputspec
13
+ from fraclab_sdk.workbench import ui_styles
14
+ from fraclab_sdk.workbench.utils import run_workspace_script
15
+
16
+ st.set_page_config(page_title="Schema Editor", page_icon="🧩", layout="wide")
17
+ st.title("Schema Editor (InputSpec)")
18
+
19
+ ui_styles.apply_global_styles()
20
+
21
+ # --- Page-Specific CSS: Editor Styling ---
22
+ st.markdown("""
23
+ <style>
24
+ textarea {
25
+ font-family: "Source Code Pro", monospace !important;
26
+ font-size: 14px !important;
27
+ background-color: #fcfcfc !important;
28
+ }
29
+ </style>
30
+ """, unsafe_allow_html=True)
31
+
32
+
33
+ def write_params_schema(ws_dir: Path) -> None:
34
+ """Generate dist/params.schema.json via subprocess."""
35
+ script = '''
36
+ import json
37
+ from schema.inputspec import INPUT_SPEC
38
+
39
+ if hasattr(INPUT_SPEC, "model_json_schema"):
40
+ schema = INPUT_SPEC.model_json_schema()
41
+ elif hasattr(INPUT_SPEC, "schema"):
42
+ schema = INPUT_SPEC.schema()
43
+ else:
44
+ raise SystemExit("INPUT_SPEC missing schema generator")
45
+ print(json.dumps(schema))
46
+ '''
47
+ result = run_workspace_script(ws_dir, script)
48
+ if result.returncode != 0:
49
+ raise RuntimeError(result.stderr or "Failed to generate params.schema.json")
50
+
51
+ try:
52
+ data = json.loads(result.stdout)
53
+ except json.JSONDecodeError as exc:
54
+ raise RuntimeError("Failed to parse generated params schema") from exc
55
+
56
+ dist_dir = ws_dir / "dist"
57
+ dist_dir.mkdir(parents=True, exist_ok=True)
58
+ (dist_dir / "params.schema.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
59
+
60
+
61
+ DOC_SUMMARY = """
62
+ **InputSpec Cheatsheet:**
63
+ - **Types**: `str`, `int`, `float`, `bool`, `datetime`, `Optional[T]`, `Literal["A", "B"]`.
64
+ - **Field**: `Field(..., title="Title", description="Desc")`.
65
+ - **UI Metadata**: `json_schema_extra=schema_extra(group="Basic", order=1, ui_type="range")`.
66
+ - **Visibility**: `show_when=show_when_condition("mode", "equals", "advanced")`.
67
+ - **Validation**: `@field_validator("field")` or `@model_validator(mode="after")`.
68
+ """
69
+
70
+ config = SDKConfig()
71
+ algo_lib = AlgorithmLibrary(config)
72
+ algos = algo_lib.list_algorithms()
73
+
74
+ if not algos:
75
+ st.info("No algorithms imported.")
76
+ st.stop()
77
+
78
+ # --- 1. Selection ---
79
+ with st.container(border=True):
80
+ c1, c2 = st.columns([3, 1])
81
+ with c1:
82
+ algo_options = {f"{a.algorithm_id}:{a.version}": a for a in algos}
83
+ selected_key = st.selectbox(
84
+ "Select Algorithm",
85
+ options=list(algo_options.keys()),
86
+ format_func=lambda k: f"{algo_options[k].algorithm_id} (v{algo_options[k].version})",
87
+ label_visibility="collapsed"
88
+ )
89
+
90
+ if not selected_key:
91
+ st.stop()
92
+
93
+ selected = algo_options[selected_key]
94
+ handle = algo_lib.get_algorithm(selected.algorithm_id, selected.version)
95
+ algo_dir = handle.directory
96
+
97
+ st.caption(f"Algorithm dir: `{algo_dir}`")
98
+ schema_dir = algo_dir / "schema"
99
+ schema_dir.mkdir(parents=True, exist_ok=True)
100
+ inputspec_path = schema_dir / "inputspec.py"
101
+
102
+ DEFAULT_INPUTSPEC = '''from __future__ import annotations
103
+ from pydantic import BaseModel, Field
104
+ from .base import schema_extra, show_when_condition, show_when_and, show_when_or
105
+
106
+ class INPUT_SPEC(BaseModel):
107
+ """Algorithm parameters."""
108
+ # datasetKey: str = Field(..., title="Dataset Key")
109
+ '''
110
+
111
+ if not inputspec_path.exists():
112
+ inputspec_path.write_text(DEFAULT_INPUTSPEC, encoding="utf-8")
113
+
114
+ # --- 2. Documentation ---
115
+ with st.expander("📚 Documentation & Tips", expanded=True):
116
+ st.markdown(DOC_SUMMARY)
117
+
118
+ # --- 3. Editor ---
119
+ content = inputspec_path.read_text(encoding="utf-8")
120
+ edited = st.text_area("inputspec.py", value=content, height=600, label_visibility="collapsed")
121
+
122
+ # --- 4. Actions ---
123
+ col_save, col_valid, col_spacer = st.columns([1, 1, 4])
124
+
125
+ with col_save:
126
+ if st.button("💾 Save & Generate", type="primary", width="stretch"):
127
+ try:
128
+ inputspec_path.write_text(edited, encoding="utf-8")
129
+ write_params_schema(algo_dir)
130
+ st.toast("Schema saved and JSON generated!", icon="✅")
131
+ except Exception as e:
132
+ st.error(f"Save failed: {e}")
133
+
134
+ with col_valid:
135
+ if st.button("🔍 Validate", type="secondary", width="stretch"):
136
+ try:
137
+ # Auto-save before validate
138
+ inputspec_path.write_text(edited, encoding="utf-8")
139
+ write_params_schema(algo_dir)
140
+
141
+ result = validate_inputspec(algo_dir)
142
+ if result.valid:
143
+ st.success("Validation Passed!", icon="✅")
144
+ 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
+ except Exception as e:
149
+ st.error(f"Validation error: {e}")
@@ -0,0 +1,144 @@
1
+ """OutputSpec editor page for editing algorithm output_contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from pathlib import Path
8
+
9
+ import streamlit as st
10
+
11
+ from fraclab_sdk.algorithm import AlgorithmLibrary
12
+ from fraclab_sdk.config import SDKConfig
13
+ from fraclab_sdk.devkit.validate import validate_output_contract
14
+ from fraclab_sdk.workbench import ui_styles
15
+ from fraclab_sdk.workbench.utils import run_workspace_script
16
+
17
+ st.set_page_config(page_title="OutputSpec Editor", page_icon="📤", layout="wide")
18
+ st.title("OutputSpec Editor")
19
+
20
+ ui_styles.apply_global_styles()
21
+
22
+ # --- Page-Specific CSS: Editor Styling ---
23
+ st.markdown("""
24
+ <style>
25
+ textarea {
26
+ font-family: "Source Code Pro", monospace !important;
27
+ font-size: 14px !important;
28
+ background-color: #fcfcfc !important;
29
+ }
30
+ </style>
31
+ """, unsafe_allow_html=True)
32
+ def write_dist_from_contract(ws_dir: Path, algo_id: str, version: str) -> None:
33
+ """Import OUTPUT_CONTRACT and dump to dist/output_contract.json."""
34
+ script = '''
35
+ import json
36
+ from schema.output_contract import OUTPUT_CONTRACT
37
+ if hasattr(OUTPUT_CONTRACT, "model_dump"):
38
+ print(json.dumps(OUTPUT_CONTRACT.model_dump(mode="json")))
39
+ else:
40
+ print(json.dumps(OUTPUT_CONTRACT.dict()))
41
+ '''
42
+ result = run_workspace_script(ws_dir, script)
43
+ if result.returncode != 0:
44
+ raise RuntimeError(result.stderr or "Failed to load OUTPUT_CONTRACT")
45
+
46
+ try:
47
+ data = json.loads(result.stdout)
48
+ except json.JSONDecodeError as exc:
49
+ raise RuntimeError("Failed to parse OUTPUT_CONTRACT output") from exc
50
+
51
+ dist_dir = ws_dir / "dist"
52
+ dist_dir.mkdir(parents=True, exist_ok=True)
53
+ (dist_dir / "output_contract.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
54
+
55
+
56
+ DOC_SUMMARY = """
57
+ **OutputSpec Cheatsheet:**
58
+ - **Datasets**: List of `OutputDatasetContract` inside `OutputContract`.
59
+ - **Props**: `key` (unique), `kind` (frame/object/blob/scalar), `owner`, `cardinality` (one/many), `required`.
60
+ - **Schema**: Must match `kind` (e.g., `ScalarSchema` for kind='scalar').
61
+ - **Dimensions**: List of string keys used in artifact dims.
62
+ """
63
+
64
+ config = SDKConfig()
65
+ algo_lib = AlgorithmLibrary(config)
66
+ algos = algo_lib.list_algorithms()
67
+
68
+ if not algos:
69
+ st.info("No algorithms imported.")
70
+ st.stop()
71
+
72
+ # --- 1. Selection ---
73
+ with st.container(border=True):
74
+ c1, c2 = st.columns([3, 1])
75
+ with c1:
76
+ algo_options = {f"{a.algorithm_id}:{a.version}": a for a in algos}
77
+ selected_key = st.selectbox(
78
+ "Select Algorithm",
79
+ options=list(algo_options.keys()),
80
+ format_func=lambda k: f"{algo_options[k].algorithm_id} (v{algo_options[k].version})",
81
+ label_visibility="collapsed"
82
+ )
83
+
84
+ if not selected_key:
85
+ st.stop()
86
+
87
+ selected = algo_options[selected_key]
88
+ handle = algo_lib.get_algorithm(selected.algorithm_id, selected.version)
89
+ workspace_dir = handle.directory
90
+
91
+ st.caption(f"Algorithm dir: `{workspace_dir}`")
92
+ schema_dir = workspace_dir / "schema"
93
+ schema_dir.mkdir(parents=True, exist_ok=True)
94
+ output_spec_path = schema_dir / "output_contract.py"
95
+
96
+ DEFAULT_OUTPUTSPEC = '''from __future__ import annotations
97
+ from fraclab_sdk.specs.output import BlobSchema, OutputContract, OutputDatasetContract, ScalarSchema
98
+
99
+ OUTPUT_CONTRACT = OutputContract(
100
+ datasets=[
101
+ # OutputDatasetContract(key="metrics", kind="scalar", cardinality="many", ...)
102
+ ]
103
+ )
104
+ '''
105
+
106
+ if not output_spec_path.exists():
107
+ output_spec_path.write_text(DEFAULT_OUTPUTSPEC, encoding="utf-8")
108
+
109
+ # --- 2. Documentation ---
110
+ with st.expander("📚 Documentation & Tips", expanded=True):
111
+ st.markdown(DOC_SUMMARY)
112
+
113
+ # --- 3. Editor ---
114
+ content = output_spec_path.read_text(encoding="utf-8")
115
+ edited = st.text_area("output_contract.py", value=content, height=600, label_visibility="collapsed")
116
+
117
+ # --- 4. Actions ---
118
+ col_save, col_valid, col_spacer = st.columns([1, 1, 4])
119
+
120
+ with col_save:
121
+ if st.button("💾 Save & Generate", type="primary", width="stretch"):
122
+ try:
123
+ output_spec_path.write_text(edited, encoding="utf-8")
124
+ write_dist_from_contract(workspace_dir, selected.algorithm_id, selected.version)
125
+ st.toast("Output spec saved and JSON generated!", icon="✅")
126
+ except Exception as e:
127
+ st.error(f"Save failed: {e}")
128
+
129
+ with col_valid:
130
+ if st.button("🔍 Validate", type="secondary", width="stretch"):
131
+ try:
132
+ # Auto-save before validate
133
+ output_spec_path.write_text(edited, encoding="utf-8")
134
+ write_dist_from_contract(workspace_dir, selected.algorithm_id, selected.version)
135
+
136
+ result = validate_output_contract(workspace_dir / "dist" / "output_contract.json")
137
+ if result.valid:
138
+ st.success("Validation Passed!", icon="✅")
139
+ else:
140
+ st.error("Validation Failed", icon="🚫")
141
+ for issue in result.issues:
142
+ st.warning(f"[{issue.severity}] {issue.code}: {issue.message}")
143
+ except Exception as e:
144
+ st.error(f"Validation error: {e}")
@@ -0,0 +1,238 @@
1
+ """Algorithm export page."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import json
7
+ import shutil
8
+ import tempfile
9
+ from pathlib import Path
10
+
11
+ import streamlit as st
12
+
13
+ from fraclab_sdk.algorithm import AlgorithmLibrary
14
+ from fraclab_sdk.config import SDKConfig
15
+ from fraclab_sdk.snapshot import SnapshotLibrary
16
+ from fraclab_sdk.workbench import ui_styles
17
+
18
+ st.set_page_config(page_title="Export Algorithm", page_icon="📦", layout="wide")
19
+ st.title("Export Algorithm")
20
+
21
+ ui_styles.apply_global_styles()
22
+
23
+ # --- Page-Specific CSS ---
24
+ st.markdown("""
25
+ <style>
26
+ /* Status badge styling */
27
+ .status-badge {
28
+ padding: 4px 8px;
29
+ border-radius: 4px;
30
+ font-weight: 600;
31
+ font-size: 0.85rem;
32
+ }
33
+ .status-ok { background-color: #d1fae5; color: #065f46; }
34
+ .status-missing { background-color: #fee2e2; color: #991b1b; }
35
+ </style>
36
+ """, unsafe_allow_html=True)
37
+
38
+
39
+ config = SDKConfig()
40
+ algo_lib = AlgorithmLibrary(config)
41
+ snap_lib = SnapshotLibrary(config)
42
+
43
+ algos = algo_lib.list_algorithms()
44
+ if not algos:
45
+ st.info("No algorithms imported. Use Snapshots page to import or create one.")
46
+ st.stop()
47
+
48
+ # ==========================================
49
+ # 1. Source Selection
50
+ # ==========================================
51
+ st.subheader("1. Select Algorithm Source")
52
+
53
+ with st.container(border=True):
54
+ c1, c2 = st.columns([3, 1])
55
+ with c1:
56
+ algo_options = {f"{a.algorithm_id}:{a.version}": a for a in algos}
57
+ selected_key = st.selectbox(
58
+ "Target Algorithm",
59
+ options=list(algo_options.keys()),
60
+ format_func=lambda k: f"{algo_options[k].algorithm_id} (v{algo_options[k].version})",
61
+ label_visibility="collapsed"
62
+ )
63
+ with c2:
64
+ if selected_key:
65
+ selected_algo = algo_options[selected_key]
66
+ st.caption(f"ID: `{selected_algo.algorithm_id}`")
67
+
68
+ if not selected_key:
69
+ st.stop()
70
+
71
+ selected_algo = algo_options[selected_key]
72
+ handle = algo_lib.get_algorithm(selected_algo.algorithm_id, selected_algo.version)
73
+ algo_dir = handle.directory
74
+
75
+ # File paths
76
+ manifest_path = algo_dir / "manifest.json"
77
+ params_schema_path = algo_dir / "dist" / "params.schema.json"
78
+ output_contract_path = algo_dir / "dist" / "output_contract.json"
79
+ drs_path = algo_dir / "dist" / "drs.json"
80
+
81
+ # ==========================================
82
+ # 2. Package Integrity Check & DRS Source
83
+ # ==========================================
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
+
98
+ 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")
137
+ else:
138
+ st.info("File not generated yet.")
139
+
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
+
144
+ # ==========================================
145
+ # 3. DRS Source Selection
146
+ # ==========================================
147
+ st.subheader("3. Select DRS Source")
148
+
149
+ snapshots = snap_lib.list_snapshots()
150
+ snapshot_map = {s.snapshot_id: s for s in snapshots}
151
+
152
+ if not snapshots:
153
+ st.warning("No snapshots available. Import a snapshot first to provide DRS for export.")
154
+ st.stop()
155
+
156
+ with st.container(border=True):
157
+ st.caption("The DRS (Data Requirement Specification) defines dataset requirements. Select a snapshot to use its DRS in the export package.")
158
+
159
+ selected_snapshot_id = st.selectbox(
160
+ "Snapshot (DRS Source)",
161
+ options=list(snapshot_map.keys()),
162
+ format_func=lambda x: f"{x} — {snapshot_map[x].bundle_id}",
163
+ label_visibility="collapsed"
164
+ )
165
+
166
+ if not selected_snapshot_id:
167
+ st.stop()
168
+
169
+ snapshot_handle = snap_lib.get_snapshot(selected_snapshot_id)
170
+
171
+ # ==========================================
172
+ # 4. Export
173
+ # ==========================================
174
+ st.divider()
175
+ st.subheader("4. Export")
176
+
177
+ def build_zip() -> bytes:
178
+ with tempfile.TemporaryDirectory() as tmpdir:
179
+ tmpdir_path = Path(tmpdir)
180
+ # copy installed algorithm content
181
+ shutil.copytree(algo_dir, tmpdir_path / algo_dir.name, dirs_exist_ok=True)
182
+ target_root = tmpdir_path / algo_dir.name
183
+
184
+ # ensure manifest files paths cover dist outputs if present
185
+ manifest_data = json.loads(manifest_path.read_text())
186
+ files = manifest_data.get("files") or {}
187
+
188
+ if output_contract_path.exists():
189
+ files["outputContractPath"] = "dist/output_contract.json"
190
+ if params_schema_path.exists():
191
+ files["paramsSchemaPath"] = "dist/params.schema.json"
192
+ if drs_path.exists():
193
+ files["drsPath"] = "dist/drs.json"
194
+
195
+ if files:
196
+ manifest_data["files"] = files
197
+
198
+ (target_root / "manifest.json").write_text(json.dumps(manifest_data, indent=2), encoding="utf-8")
199
+
200
+ # DRS Override Logic
201
+ # Try to find DRS path from manifest, default to dist/drs.json
202
+ drs_rel_path = manifest_data.get("files", {}).get("drsPath", "dist/drs.json")
203
+ target_drs_path = target_root / drs_rel_path
204
+ target_drs_path.parent.mkdir(parents=True, exist_ok=True)
205
+
206
+ # Read DRS from Snapshot
207
+ snap_drs_path = snapshot_handle.directory / snapshot_handle.manifest.specFiles.drsPath
208
+
209
+ if snap_drs_path.exists():
210
+ target_drs_path.write_bytes(snap_drs_path.read_bytes())
211
+ else:
212
+ # Fallback if snapshot DRS is missing structure (rare)
213
+ pass
214
+
215
+ # Zip it up
216
+ 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)
218
+ zip_path = tmpdir_path / "algorithm_export.zip"
219
+ zip_buf.write(zip_path.read_bytes())
220
+ zip_buf.seek(0)
221
+ return zip_buf.read()
222
+
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}")
@@ -0,0 +1 @@
1
+ """Streamlit multipage registry for the workbench."""