ivoryos 0.1.22__tar.gz → 0.1.24__tar.gz
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.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- {ivoryos-0.1.22/ivoryos.egg-info → ivoryos-0.1.24}/PKG-INFO +1 -1
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/__init__.py +23 -5
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/database/database.py +20 -4
- ivoryos-0.1.24/ivoryos/routes/database/templates/database/experiment_step_view.html +130 -0
- ivoryos-0.1.24/ivoryos/routes/database/templates/database/step_card.html +7 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/design/design.py +7 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/design/templates/design/experiment_builder.html +57 -10
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/design/templates/design/experiment_run.html +3 -2
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/js/socket_handler.js +5 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/form.py +52 -7
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/script_runner.py +22 -4
- ivoryos-0.1.24/ivoryos/version.py +1 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24/ivoryos.egg-info}/PKG-INFO +1 -1
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos.egg-info/SOURCES.txt +2 -0
- ivoryos-0.1.22/ivoryos/version.py +0 -1
- {ivoryos-0.1.22 → ivoryos-0.1.24}/LICENSE +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/MANIFEST.in +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/README.md +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/config.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/auth/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/auth/auth.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/auth/templates/auth/login.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/auth/templates/auth/signup.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/control.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/templates/control/controllers.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/templates/control/controllers_home.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/templates/control/controllers_new.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/database/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/database/templates/database/experiment_database.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/database/templates/database/workflow_run_database.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/design/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/main/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/main/main.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/main/templates/main/help.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/main/templates/main/home.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/favicon.ico +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/gui_annotation/Slide1.png +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/js/overlay.js +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/js/sortable_card.js +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/js/sortable_design.js +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/logo.webp +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/static/style.css +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/templates/base.html +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/__init__.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/client_proxy.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/db_models.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/global_config.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/llm_agent.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/utils/utils.py +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos.egg-info/dependency_links.txt +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos.egg-info/requires.txt +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos.egg-info/top_level.txt +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/setup.cfg +0 -0
- {ivoryos-0.1.22 → ivoryos-0.1.24}/setup.py +0 -0
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import sys
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
|
-
from flask import Flask, redirect, url_for, g
|
|
5
|
+
from flask import Flask, redirect, url_for, g, Blueprint
|
|
6
6
|
|
|
7
7
|
from ivoryos.config import Config, get_config
|
|
8
8
|
from ivoryos.routes.auth.auth import auth, login_manager
|
|
@@ -81,7 +81,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
81
81
|
config: Config = None,
|
|
82
82
|
logger: Union[str, list] = None,
|
|
83
83
|
logger_output_name: str = None,
|
|
84
|
-
enable_design=True
|
|
84
|
+
enable_design=True,
|
|
85
|
+
blueprint_plugins=None,
|
|
85
86
|
):
|
|
86
87
|
"""
|
|
87
88
|
Start ivoryOS app server.
|
|
@@ -99,7 +100,11 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
99
100
|
"""
|
|
100
101
|
app = create_app(config_class=config or get_config()) # Create app instance using factory function
|
|
101
102
|
|
|
102
|
-
plugins =
|
|
103
|
+
# plugins = load_installed_plugins(app, socketio)
|
|
104
|
+
plugins = {}
|
|
105
|
+
if blueprint_plugins:
|
|
106
|
+
config_plugins = load_plugins(blueprint_plugins, app, socketio)
|
|
107
|
+
plugins.extend(config_plugins)
|
|
103
108
|
|
|
104
109
|
def inject_nav_config():
|
|
105
110
|
"""Make NAV_CONFIG available globally to all templates."""
|
|
@@ -143,7 +148,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
143
148
|
# return app
|
|
144
149
|
|
|
145
150
|
|
|
146
|
-
def
|
|
151
|
+
def load_installed_plugins(app, socketio):
|
|
147
152
|
"""
|
|
148
153
|
Dynamically load installed plugins and attach Flask-SocketIO.
|
|
149
154
|
"""
|
|
@@ -160,4 +165,17 @@ def load_plugins(app, socketio):
|
|
|
160
165
|
|
|
161
166
|
return plugin_names
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
def load_plugins(blueprints:list[Blueprint], app, socketio):
|
|
169
|
+
"""
|
|
170
|
+
Dynamically load installed plugins and attach Flask-SocketIO.
|
|
171
|
+
"""
|
|
172
|
+
plugin_names = []
|
|
173
|
+
if not isinstance(blueprints, list):
|
|
174
|
+
blueprints = [blueprints]
|
|
175
|
+
for blueprint in blueprints:
|
|
176
|
+
# If the plugin has an `init_socketio()` function, pass socketio
|
|
177
|
+
if hasattr(blueprint, 'init_socketio'):
|
|
178
|
+
blueprint.init_socketio(socketio)
|
|
179
|
+
plugin_names.append(blueprint.name)
|
|
180
|
+
app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
|
|
181
|
+
return plugin_names
|
|
@@ -203,11 +203,27 @@ def list_workflows():
|
|
|
203
203
|
return render_template('workflow_run_database.html', workflows=workflows)
|
|
204
204
|
|
|
205
205
|
|
|
206
|
-
@database.route(
|
|
206
|
+
@database.route("/workflow_steps/<int:workflow_id>")
|
|
207
207
|
def get_workflow_steps(workflow_id):
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
workflow = WorkflowRun.query.get_or_404(workflow_id)
|
|
209
|
+
steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
|
|
210
|
+
|
|
211
|
+
# Organize steps by phase + repeat_index
|
|
212
|
+
grouped = {
|
|
213
|
+
"prep": [],
|
|
214
|
+
"script": {},
|
|
215
|
+
"cleanup": [],
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for step in steps:
|
|
219
|
+
if step.phase == "prep":
|
|
220
|
+
grouped["prep"].append(step)
|
|
221
|
+
elif step.phase == "script":
|
|
222
|
+
grouped["script"].setdefault(step.repeat_index, []).append(step)
|
|
223
|
+
elif step.phase == "cleanup" or step.method_name == "stop":
|
|
224
|
+
grouped["cleanup"].append(step)
|
|
225
|
+
|
|
226
|
+
return render_template("experiment_step_view.html", workflow=workflow, grouped=grouped)
|
|
211
227
|
|
|
212
228
|
|
|
213
229
|
@database.route("/delete_workflow_data/<workflow_id>")
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block title %}IvoryOS | Experiment Results{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block body %}
|
|
6
|
+
<style>
|
|
7
|
+
.vis-time-axis .vis-text.vis-minor,
|
|
8
|
+
.vis-time-axis .vis-text.vis-major {
|
|
9
|
+
color: #666;
|
|
10
|
+
}
|
|
11
|
+
.vis-item.stop {
|
|
12
|
+
background-color: red;
|
|
13
|
+
color: white;
|
|
14
|
+
border: none;
|
|
15
|
+
font-weight: bold;
|
|
16
|
+
}
|
|
17
|
+
</style>
|
|
18
|
+
|
|
19
|
+
<div id="timeline"></div>
|
|
20
|
+
|
|
21
|
+
<script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
|
|
22
|
+
<link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
|
|
23
|
+
|
|
24
|
+
<h1>Experiment Step View</h1>
|
|
25
|
+
|
|
26
|
+
<div id="visualization"></div>
|
|
27
|
+
|
|
28
|
+
<script type="text/javascript">
|
|
29
|
+
var container = document.getElementById('visualization');
|
|
30
|
+
|
|
31
|
+
const items = [
|
|
32
|
+
{% if grouped.prep %}
|
|
33
|
+
{
|
|
34
|
+
id: 'prep',
|
|
35
|
+
content: 'Prep Phase',
|
|
36
|
+
start: '{{ grouped.prep[0].start_time }}',
|
|
37
|
+
end: '{{ grouped.prep[-1].end_time }}',
|
|
38
|
+
className: 'prep',
|
|
39
|
+
group: 'prep'
|
|
40
|
+
},
|
|
41
|
+
{% endif %}
|
|
42
|
+
|
|
43
|
+
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
44
|
+
{
|
|
45
|
+
id: 'iter{{ repeat_index }}',
|
|
46
|
+
content: 'Iteration {{ repeat_index }}',
|
|
47
|
+
start: '{{ step_list[0].start_time }}',
|
|
48
|
+
end: '{{ step_list[-1].end_time }}',
|
|
49
|
+
className: 'script',
|
|
50
|
+
group: 'iter{{ repeat_index }}'
|
|
51
|
+
},
|
|
52
|
+
{% for step in step_list %}
|
|
53
|
+
{% if step.method_name == "stop" %}
|
|
54
|
+
{
|
|
55
|
+
id: 'stop-{{ step.id }}',
|
|
56
|
+
content: '🛑 Stop',
|
|
57
|
+
start: '{{ step.start_time }}',
|
|
58
|
+
type: 'point',
|
|
59
|
+
className: 'stop',
|
|
60
|
+
group: 'iter{{ repeat_index }}'
|
|
61
|
+
},
|
|
62
|
+
{% endif %}
|
|
63
|
+
{% endfor %}
|
|
64
|
+
{% endfor %}
|
|
65
|
+
|
|
66
|
+
{% if grouped.cleanup %}
|
|
67
|
+
{
|
|
68
|
+
id: 'cleanup',
|
|
69
|
+
content: 'Cleanup Phase',
|
|
70
|
+
start: '{{ grouped.cleanup[0].start_time }}',
|
|
71
|
+
end: '{{ grouped.cleanup[-1].end_time }}',
|
|
72
|
+
className: 'cleanup',
|
|
73
|
+
group: 'cleanup'
|
|
74
|
+
|
|
75
|
+
},
|
|
76
|
+
{% endif %}
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const groups = [
|
|
80
|
+
{% if grouped.prep %}{ id: 'prep', content: 'Prep' },{% endif %}
|
|
81
|
+
{% for repeat_index in grouped.script.keys()|sort %}{ id: 'iter{{ repeat_index }}', content: 'Iteration {{ repeat_index }}' },{% endfor %}
|
|
82
|
+
{% if grouped.cleanup %}{ id: 'cleanup', content: 'Cleanup' },{% endif %}
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
var options = {
|
|
86
|
+
clickToUse: true,
|
|
87
|
+
stack: false, // important to keep point within group row
|
|
88
|
+
horizontalScroll: true,
|
|
89
|
+
zoomKey: 'ctrlKey'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Initialize your timeline with the sorted groups
|
|
93
|
+
const timeline = new vis.Timeline(container, items, groups, options);
|
|
94
|
+
|
|
95
|
+
timeline.on('select', function (props) {
|
|
96
|
+
const id = props.items[0];
|
|
97
|
+
if (id && id.startsWith('iter')) {
|
|
98
|
+
const card = document.getElementById('card-' + id);
|
|
99
|
+
if (card) {
|
|
100
|
+
const yOffset = -80;
|
|
101
|
+
const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
|
|
102
|
+
window.scrollTo({ top: y, behavior: 'smooth' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<h2>Workflow: {{ workflow.name }}</h2>
|
|
109
|
+
|
|
110
|
+
{% if grouped.prep %}
|
|
111
|
+
<h4 class="mt-4">Prep Phase</h4>
|
|
112
|
+
{% for step in grouped.prep %}
|
|
113
|
+
{% include "step_card.html" %}
|
|
114
|
+
{% endfor %}
|
|
115
|
+
{% endif %}
|
|
116
|
+
|
|
117
|
+
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
118
|
+
<h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
|
|
119
|
+
{% for step in step_list %}
|
|
120
|
+
{% include "step_card.html" %}
|
|
121
|
+
{% endfor %}
|
|
122
|
+
{% endfor %}
|
|
123
|
+
|
|
124
|
+
{% if grouped.cleanup %}
|
|
125
|
+
<h4 class="mt-4">Cleanup Phase</h4>
|
|
126
|
+
{% for step in grouped.cleanup %}
|
|
127
|
+
{% include "step_card.html" %}
|
|
128
|
+
{% endfor %}
|
|
129
|
+
{% endif %}
|
|
130
|
+
{% endblock %}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<div class="card mb-2 {{ 'border-danger text-danger bg-light' if step.run_error else 'border-secondary' }}">
|
|
2
|
+
<div class="card-body p-2">
|
|
3
|
+
<strong>{{ step.method_name }}</strong>
|
|
4
|
+
<small>Start: {{ step.start_time }}</small>
|
|
5
|
+
<small>End: {{ step.end_time }}</small>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
@@ -40,6 +40,13 @@ def handle_abort_current():
|
|
|
40
40
|
|
|
41
41
|
@socketio.on('pause')
|
|
42
42
|
def handle_pause():
|
|
43
|
+
runner.retry = False
|
|
44
|
+
msg = runner.toggle_pause()
|
|
45
|
+
socketio.emit('log', {'message': msg})
|
|
46
|
+
|
|
47
|
+
@socketio.on('retry')
|
|
48
|
+
def handle_pause():
|
|
49
|
+
runner.retry = True
|
|
43
50
|
msg = runner.toggle_pause()
|
|
44
51
|
socketio.emit('log', {'message': msg})
|
|
45
52
|
|
{ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/design/templates/design/experiment_builder.html
RENAMED
|
@@ -144,6 +144,16 @@
|
|
|
144
144
|
{{ field(class="btn btn-dark") }}
|
|
145
145
|
{% elif field.type == "BooleanField" %}
|
|
146
146
|
{{ field(class="form-check-input") }}
|
|
147
|
+
{% elif field.type == "FlexibleEnumField" %}
|
|
148
|
+
<input type="text" id="{{ field.id }}" name="{{ field.name }}" value="{{ field.data }}"
|
|
149
|
+
list="{{ field.id }}_options" placeholder="{{ field.render_kw.placeholder if field.render_kw and field.render_kw.placeholder }}"
|
|
150
|
+
class="form-control">
|
|
151
|
+
<datalist id="{{ field.id }}_options">
|
|
152
|
+
{% for key in field.choices %}
|
|
153
|
+
<option value="{{ key }}">{{ key }}</option>
|
|
154
|
+
{% endfor %}
|
|
155
|
+
</datalist>
|
|
156
|
+
|
|
147
157
|
{% else %}
|
|
148
158
|
{{ field(class="form-control") }}
|
|
149
159
|
{% endif %}
|
|
@@ -219,7 +229,7 @@
|
|
|
219
229
|
|
|
220
230
|
{# canvas #}
|
|
221
231
|
<div class="col-md-9 scroll-column">
|
|
222
|
-
<div>
|
|
232
|
+
<div class="d-flex align-items-center ">
|
|
223
233
|
{# file dropdown menu #}
|
|
224
234
|
<ul class="nav nav-tabs">
|
|
225
235
|
<li class="nav-item dropdown">
|
|
@@ -242,6 +252,12 @@
|
|
|
242
252
|
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='script' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='script') }}">Experiment</a></li>
|
|
243
253
|
<li class="nav-item"><a class="{{'nav-link active' if script.editing_type=='cleanup' else 'nav-link'}}" aria-current="page" href="{{url_for('design.toggle_script_type', stype='cleanup') }}">Clean up</a></li>
|
|
244
254
|
</ul>
|
|
255
|
+
|
|
256
|
+
<div class="form-check form-switch ms-auto">
|
|
257
|
+
<input class="form-check-input" type="checkbox" id="toggleLineNumbers" onchange="toggleLineNumbers()">
|
|
258
|
+
<label class="form-check-label" for="toggleLineNumbers">Show Line Numbers</label>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
245
261
|
</div>
|
|
246
262
|
|
|
247
263
|
<div class="canvas" droppable="true">
|
|
@@ -279,6 +295,7 @@
|
|
|
279
295
|
<ul class="reorder">
|
|
280
296
|
{% for button in buttons %}
|
|
281
297
|
<li id="{{ button['id'] }}" style="list-style-type: none;">
|
|
298
|
+
<span class="line-number d-none">{{ button['id'] }}.</span>
|
|
282
299
|
<a href="{{ url_for('design.edit_action', uuid=button['uuid']) }}" type="button" class="btn btn-light" style="{{ button['style'] }}">{{ button['label'] }}</a>
|
|
283
300
|
{% if not button["instrument"] in ["if","while","repeat"] %}
|
|
284
301
|
<a href="{{ url_for('design.duplicate_action', id=button['id']) }}" type="button" class="btn btn-light"><span class="bi bi-copy"></span></a>
|
|
@@ -406,14 +423,44 @@
|
|
|
406
423
|
</div>
|
|
407
424
|
|
|
408
425
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
</script>
|
|
413
|
-
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
|
414
|
-
{% endif %}
|
|
415
|
-
<script>
|
|
416
|
-
const updateListUrl = "{{ url_for('design.update_list') }}";
|
|
426
|
+
{% if instrument and use_llm %}
|
|
427
|
+
<script>
|
|
428
|
+
const buttonIds = {{ ['generate'] | tojson }};
|
|
417
429
|
</script>
|
|
418
|
-
<script src="{{ url_for('static', filename='js/
|
|
430
|
+
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
|
431
|
+
{% endif %}
|
|
432
|
+
|
|
433
|
+
<script>
|
|
434
|
+
const updateListUrl = "{{ url_for('design.update_list') }}";
|
|
435
|
+
|
|
436
|
+
// Toggle visibility of line numbers
|
|
437
|
+
function toggleLineNumbers(save = true) {
|
|
438
|
+
const show = document.getElementById('toggleLineNumbers').checked;
|
|
439
|
+
document.querySelectorAll('.line-number').forEach(el => {
|
|
440
|
+
el.classList.toggle('d-none', !show);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (save) {
|
|
444
|
+
localStorage.setItem('showLineNumbers', show ? 'true' : 'false');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Restore state on page load
|
|
449
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
450
|
+
const savedState = localStorage.getItem('showLineNumbers');
|
|
451
|
+
const checkbox = document.getElementById('toggleLineNumbers');
|
|
452
|
+
|
|
453
|
+
if (savedState === 'true') {
|
|
454
|
+
checkbox.checked = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
toggleLineNumbers(false); // don't overwrite localStorage on load
|
|
458
|
+
|
|
459
|
+
checkbox.addEventListener('change', () => toggleLineNumbers());
|
|
460
|
+
});
|
|
461
|
+
</script>
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
<script src="{{ url_for('static', filename='js/sortable_design.js') }}"></script>
|
|
465
|
+
|
|
419
466
|
{% endblock %}
|
{ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/design/templates/design/experiment_run.html
RENAMED
|
@@ -423,8 +423,9 @@
|
|
|
423
423
|
<p>Do you want to continue execution or stop?</p>
|
|
424
424
|
</div>
|
|
425
425
|
<div class="modal-footer">
|
|
426
|
-
|
|
427
|
-
|
|
426
|
+
<button type="button" class="btn btn-primary" id="retry-btn" data-bs-dismiss="modal">Rerun Current Step</button>
|
|
427
|
+
<button type="button" class="btn btn-success" id="continue-btn" data-bs-dismiss="modal">Continue</button>
|
|
428
|
+
<button type="button" class="btn btn-danger" id="stop-btn" data-bs-dismiss="modal">Stop Execution</button>
|
|
428
429
|
</div>
|
|
429
430
|
</div>
|
|
430
431
|
</div>
|
|
@@ -68,6 +68,11 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
68
68
|
console.log("Execution resumed.");
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
document.getElementById('retry-btn').addEventListener('click', function() {
|
|
72
|
+
socket.emit('retry'); // Resume execution
|
|
73
|
+
console.log("Execution resumed, retrying.");
|
|
74
|
+
});
|
|
75
|
+
|
|
71
76
|
document.getElementById('stop-btn').addEventListener('click', function() {
|
|
72
77
|
socket.emit('pause'); // Resume execution
|
|
73
78
|
socket.emit('abort_current'); // Stop execution
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
1
3
|
from wtforms.fields.choices import SelectField
|
|
2
4
|
from wtforms.fields.core import Field
|
|
3
|
-
from wtforms.validators import InputRequired
|
|
5
|
+
from wtforms.validators import InputRequired, ValidationError
|
|
4
6
|
from wtforms.widgets.core import TextInput
|
|
5
7
|
|
|
6
8
|
from flask_wtf import FlaskForm
|
|
@@ -187,6 +189,30 @@ class VariableOrBoolField(BooleanField):
|
|
|
187
189
|
return "y"
|
|
188
190
|
|
|
189
191
|
|
|
192
|
+
class FlexibleEnumField(StringField):
|
|
193
|
+
def __init__(self, label=None, validators=None, choices=None, script=None, **kwargs):
|
|
194
|
+
super().__init__(label, validators, **kwargs)
|
|
195
|
+
self.script = script
|
|
196
|
+
self.enum_class = choices
|
|
197
|
+
self.choices = [e.name for e in self.enum_class]
|
|
198
|
+
# self.value_list = [e.name for e in self.enum_class]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def process_formdata(self, valuelist):
|
|
202
|
+
if valuelist:
|
|
203
|
+
key = valuelist[0]
|
|
204
|
+
if key in self.choices:
|
|
205
|
+
# Convert the string key to Enum instance
|
|
206
|
+
self.data = self.enum_class[key].value
|
|
207
|
+
elif self.data.startswith("#"):
|
|
208
|
+
if not self.script.editing_type == "script":
|
|
209
|
+
raise ValueError(self.gettext("Variable is not supported in prep/cleanup"))
|
|
210
|
+
self.data = self.data
|
|
211
|
+
else:
|
|
212
|
+
raise ValidationError(
|
|
213
|
+
f"Invalid choice: '{key}'. Must match one of {list(self.enum_class.__members__.keys())}")
|
|
214
|
+
|
|
215
|
+
|
|
190
216
|
def format_name(name):
|
|
191
217
|
"""Converts 'example_name' to 'Example Name'."""
|
|
192
218
|
name = name.split(".")[-1]
|
|
@@ -218,20 +244,39 @@ def create_form_for_method(method, autofill, script=None, design=True):
|
|
|
218
244
|
if param.name == 'self':
|
|
219
245
|
continue
|
|
220
246
|
formatted_param_name = format_name(param.name)
|
|
247
|
+
|
|
248
|
+
default_value = None
|
|
249
|
+
if autofill:
|
|
250
|
+
default_value = f'#{param.name}'
|
|
251
|
+
else:
|
|
252
|
+
if param.default is not param.empty:
|
|
253
|
+
if isinstance(param.default, Enum):
|
|
254
|
+
default_value = param.default.name
|
|
255
|
+
else:
|
|
256
|
+
default_value = param.default
|
|
257
|
+
|
|
221
258
|
field_kwargs = {
|
|
222
259
|
"label": formatted_param_name,
|
|
223
|
-
"default":
|
|
260
|
+
"default": default_value,
|
|
224
261
|
"validators": [InputRequired()] if param.default is param.empty else None,
|
|
225
262
|
**({"script": script} if (autofill or design) else {})
|
|
226
263
|
}
|
|
227
|
-
|
|
228
|
-
param.annotation
|
|
229
|
-
|
|
230
|
-
|
|
264
|
+
if isinstance(param.annotation, type) and issubclass(param.annotation, Enum):
|
|
265
|
+
# enum_class = [(e.name, e.value) for e in param.annotation]
|
|
266
|
+
field_class = FlexibleEnumField
|
|
267
|
+
placeholder_text = f"Choose or type a value for {param.annotation.__name__} (start with # for custom)"
|
|
268
|
+
extra_kwargs = {"choices": param.annotation}
|
|
269
|
+
else:
|
|
270
|
+
field_class, placeholder_text = annotation_mapping.get(
|
|
271
|
+
param.annotation,
|
|
272
|
+
(VariableOrStringField if design else StringField, f'Enter {param.annotation} value')
|
|
273
|
+
)
|
|
274
|
+
extra_kwargs = {}
|
|
275
|
+
|
|
231
276
|
render_kwargs = {"placeholder": placeholder_text}
|
|
232
277
|
|
|
233
278
|
# Create the field with additional rendering kwargs for placeholder text
|
|
234
|
-
field = field_class(**field_kwargs, render_kw=render_kwargs)
|
|
279
|
+
field = field_class(**field_kwargs, render_kw=render_kwargs, **extra_kwargs)
|
|
235
280
|
setattr(DynamicForm, param.name, field)
|
|
236
281
|
|
|
237
282
|
# setattr(DynamicForm, f'add', fname)
|
|
@@ -17,6 +17,7 @@ deck = None
|
|
|
17
17
|
|
|
18
18
|
class ScriptRunner:
|
|
19
19
|
def __init__(self, globals_dict=None):
|
|
20
|
+
self.retry = False
|
|
20
21
|
if globals_dict is None:
|
|
21
22
|
globals_dict = globals()
|
|
22
23
|
self.globals_dict = globals_dict
|
|
@@ -143,7 +144,12 @@ class ScriptRunner:
|
|
|
143
144
|
# if line.startswith("registered_workflows"):
|
|
144
145
|
# line = line.replace("registered_workflows.", "")
|
|
145
146
|
try:
|
|
146
|
-
|
|
147
|
+
if line.startswith("time.sleep("): # add safe sleep for time.sleep lines
|
|
148
|
+
duration_str = line[len("time.sleep("):-1]
|
|
149
|
+
duration = float(duration_str)
|
|
150
|
+
self.safe_sleep(duration)
|
|
151
|
+
else:
|
|
152
|
+
exec(line, exec_globals, exec_locals)
|
|
147
153
|
step.run_error = False
|
|
148
154
|
except Exception as e:
|
|
149
155
|
logger.error(f"Error during script execution: {e}")
|
|
@@ -152,14 +158,18 @@ class ScriptRunner:
|
|
|
152
158
|
step.run_error = True
|
|
153
159
|
self.toggle_pause()
|
|
154
160
|
step.end_time = datetime.now()
|
|
161
|
+
db.session.add(step)
|
|
162
|
+
db.session.commit()
|
|
163
|
+
|
|
155
164
|
self.pause_event.wait()
|
|
156
165
|
|
|
157
166
|
# todo update script during the run
|
|
158
167
|
# _func_str = script.compile()
|
|
159
168
|
# step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
169
|
+
if not step.run_error:
|
|
170
|
+
index += 1
|
|
171
|
+
elif not self.retry:
|
|
172
|
+
index += 1
|
|
163
173
|
return exec_locals # Return the 'results' variable
|
|
164
174
|
|
|
165
175
|
def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
|
|
@@ -290,3 +300,11 @@ class ScriptRunner:
|
|
|
290
300
|
@staticmethod
|
|
291
301
|
def _emit_progress(socketio, progress):
|
|
292
302
|
socketio.emit('progress', {'progress': progress})
|
|
303
|
+
|
|
304
|
+
def safe_sleep(self, duration: float):
|
|
305
|
+
interval = 1 # check every 1 second
|
|
306
|
+
end_time = time.time() + duration
|
|
307
|
+
while time.time() < end_time:
|
|
308
|
+
if self.stop_current_event.is_set():
|
|
309
|
+
return # Exit early if stop is requested
|
|
310
|
+
time.sleep(min(interval, end_time - time.time()))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.24"
|
|
@@ -23,6 +23,8 @@ ivoryos/routes/control/templates/control/controllers_new.html
|
|
|
23
23
|
ivoryos/routes/database/__init__.py
|
|
24
24
|
ivoryos/routes/database/database.py
|
|
25
25
|
ivoryos/routes/database/templates/database/experiment_database.html
|
|
26
|
+
ivoryos/routes/database/templates/database/experiment_step_view.html
|
|
27
|
+
ivoryos/routes/database/templates/database/step_card.html
|
|
26
28
|
ivoryos/routes/database/templates/database/workflow_run_database.html
|
|
27
29
|
ivoryos/routes/design/__init__.py
|
|
28
30
|
ivoryos/routes/design/design.py
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.22"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/templates/control/controllers_home.html
RENAMED
|
File without changes
|
{ivoryos-0.1.22 → ivoryos-0.1.24}/ivoryos/routes/control/templates/control/controllers_new.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|