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.
- README.md +71 -7
- fraclab_sdk/specs/__init__.py +22 -0
- fraclab_sdk/specs/output.py +33 -0
- fraclab_sdk/workbench/Home.py +162 -0
- fraclab_sdk/workbench/__init__.py +4 -0
- fraclab_sdk/workbench/__main__.py +48 -0
- fraclab_sdk/workbench/pages/1_Snapshots.py +546 -0
- fraclab_sdk/workbench/pages/2_Browse.py +513 -0
- fraclab_sdk/workbench/pages/3_Selection.py +464 -0
- fraclab_sdk/workbench/pages/4_Run.py +325 -0
- fraclab_sdk/workbench/pages/5_Results.py +292 -0
- fraclab_sdk/workbench/pages/6_Algorithm_Edit.py +116 -0
- fraclab_sdk/workbench/pages/7_Schema_Edit.py +149 -0
- fraclab_sdk/workbench/pages/8_Output_Edit.py +144 -0
- fraclab_sdk/workbench/pages/9_Export_Algorithm.py +238 -0
- fraclab_sdk/workbench/pages/__init__.py +1 -0
- fraclab_sdk/workbench/ui_styles.py +91 -0
- fraclab_sdk/workbench/utils.py +43 -0
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.1.dist-info}/METADATA +75 -8
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.1.dist-info}/RECORD +22 -5
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.1.dist-info}/entry_points.txt +1 -0
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.1.dist-info}/WHEEL +0 -0
|
@@ -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."""
|