fraclab-sdk 0.1.0__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 +73 -7
- fraclab_sdk/__init__.py +3 -0
- fraclab_sdk/devkit/__init__.py +8 -0
- fraclab_sdk/devkit/validate.py +836 -75
- fraclab_sdk/specs/__init__.py +22 -0
- fraclab_sdk/specs/output.py +33 -0
- fraclab_sdk/version.py +5 -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 +577 -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 +331 -0
- fraclab_sdk/workbench/pages/5_Results.py +298 -0
- fraclab_sdk/workbench/pages/6_Algorithm_Edit.py +116 -0
- fraclab_sdk/workbench/pages/7_Schema_Edit.py +160 -0
- fraclab_sdk/workbench/pages/8_Output_Edit.py +155 -0
- fraclab_sdk/workbench/pages/9_Export_Algorithm.py +386 -0
- fraclab_sdk/workbench/pages/__init__.py +1 -0
- fraclab_sdk/workbench/ui_styles.py +103 -0
- fraclab_sdk/workbench/utils.py +43 -0
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.2.dist-info}/METADATA +77 -8
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.2.dist-info}/RECORD +27 -8
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.2.dist-info}/entry_points.txt +1 -0
- {fraclab_sdk-0.1.0.dist-info → fraclab_sdk-0.1.2.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", initial_sidebar_state="expanded")
|
|
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,160 @@
|
|
|
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", initial_sidebar_state="expanded")
|
|
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
|
+
if result.warnings:
|
|
144
|
+
st.warning(f"Validation Passed with {len(result.warnings)} warning(s)", icon="⚠️")
|
|
145
|
+
else:
|
|
146
|
+
st.success("Validation Passed!", icon="✅")
|
|
147
|
+
else:
|
|
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}")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
st.error(f"Validation error: {e}")
|
|
@@ -0,0 +1,155 @@
|
|
|
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", initial_sidebar_state="expanded")
|
|
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)
|
|
137
|
+
if result.valid:
|
|
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="✅")
|
|
142
|
+
else:
|
|
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}")
|
|
154
|
+
except Exception as e:
|
|
155
|
+
st.error(f"Validation error: {e}")
|