ivoryos 1.0.0__py3-none-any.whl → 1.0.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.
Potentially problematic release.
This version of ivoryos might be problematic. Click here for more details.
- ivoryos/routes/control/control.py +11 -17
- ivoryos/routes/database/database.py +42 -3
- ivoryos/routes/design/design.py +123 -27
- ivoryos/routes/design/templates/design/experiment_run.html +62 -123
- ivoryos/static/js/socket_handler.js +13 -8
- ivoryos/utils/bo_campaign.py +87 -0
- ivoryos/utils/db_models.py +27 -7
- ivoryos/utils/global_config.py +16 -7
- ivoryos/utils/script_runner.py +55 -40
- ivoryos/utils/task_runner.py +81 -0
- ivoryos/utils/utils.py +0 -68
- ivoryos/version.py +1 -1
- {ivoryos-1.0.0.dist-info → ivoryos-1.0.1.dist-info}/METADATA +3 -3
- {ivoryos-1.0.0.dist-info → ivoryos-1.0.1.dist-info}/RECORD +17 -15
- {ivoryos-1.0.0.dist-info → ivoryos-1.0.1.dist-info}/LICENSE +0 -0
- {ivoryos-1.0.0.dist-info → ivoryos-1.0.1.dist-info}/WHEEL +0 -0
- {ivoryos-1.0.0.dist-info → ivoryos-1.0.1.dist-info}/top_level.txt +0 -0
|
@@ -6,8 +6,10 @@ from flask_login import login_required
|
|
|
6
6
|
from ivoryos.utils.global_config import GlobalConfig
|
|
7
7
|
from ivoryos.utils import utils
|
|
8
8
|
from ivoryos.utils.form import create_form_from_module, format_name
|
|
9
|
+
from ivoryos.utils.task_runner import TaskRunner
|
|
9
10
|
|
|
10
11
|
global_config = GlobalConfig()
|
|
12
|
+
runner = TaskRunner()
|
|
11
13
|
|
|
12
14
|
control = Blueprint('control', __name__, template_folder='templates/control')
|
|
13
15
|
|
|
@@ -142,7 +144,9 @@ def controllers(instrument: str):
|
|
|
142
144
|
if form and form.validate_on_submit():
|
|
143
145
|
try:
|
|
144
146
|
kwargs.pop("hidden_name")
|
|
145
|
-
output =
|
|
147
|
+
output = runner.run_single_step(instrument, method_name, kwargs, wait=True,
|
|
148
|
+
current_app=current_app._get_current_object())
|
|
149
|
+
# output = function_executable(**kwargs)
|
|
146
150
|
flash(f"\nRun Success! Output value: {output}.")
|
|
147
151
|
except Exception as e:
|
|
148
152
|
flash(e.__str__())
|
|
@@ -170,24 +174,14 @@ def backend_control(instrument: str=None):
|
|
|
170
174
|
forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
|
|
171
175
|
|
|
172
176
|
if request.method == 'POST':
|
|
173
|
-
|
|
174
|
-
method_name = all_kwargs.pop("hidden_name", None)
|
|
175
|
-
# if method_name is not None:
|
|
177
|
+
method_name = request.form.get("hidden_name", None)
|
|
176
178
|
form = forms.get(method_name, None)
|
|
177
|
-
kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
|
|
178
|
-
function_executable = getattr(inst_object, method_name)
|
|
179
179
|
if form:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
except Exception as e:
|
|
186
|
-
json_output = jsonify(e.__str__())
|
|
187
|
-
return json_output, 400
|
|
188
|
-
else:
|
|
189
|
-
return "instrument not exist", 400
|
|
190
|
-
return json_output, 200
|
|
180
|
+
kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
|
|
181
|
+
wait = request.form.get("hidden_wait", "true") == "true"
|
|
182
|
+
output = runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
|
183
|
+
current_app=current_app._get_current_object())
|
|
184
|
+
return jsonify(output), 200
|
|
191
185
|
|
|
192
186
|
|
|
193
187
|
@control.route("/backend_control", methods=['GET'])
|
|
@@ -29,6 +29,11 @@ def edit_workflow(workflow_name):
|
|
|
29
29
|
off_line = current_app.config["OFF_LINE"]
|
|
30
30
|
if off_line and pseudo_name and not script.deck == pseudo_name:
|
|
31
31
|
flash(f"Choose the deck with name {script.deck}")
|
|
32
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
33
|
+
return jsonify({
|
|
34
|
+
"script": script.as_dict(),
|
|
35
|
+
"python_script": script.compile(),
|
|
36
|
+
})
|
|
32
37
|
return redirect(url_for('design.experiment_builder'))
|
|
33
38
|
|
|
34
39
|
|
|
@@ -101,7 +106,7 @@ def finalize():
|
|
|
101
106
|
return redirect(url_for('design.experiment_builder'))
|
|
102
107
|
|
|
103
108
|
|
|
104
|
-
@database.route("/database/")
|
|
109
|
+
@database.route("/database/", strict_slashes=False)
|
|
105
110
|
@database.route("/database/<deck_name>")
|
|
106
111
|
@login_required
|
|
107
112
|
def load_from_database(deck_name=None):
|
|
@@ -131,7 +136,15 @@ def load_from_database(deck_name=None):
|
|
|
131
136
|
per_page = 10
|
|
132
137
|
|
|
133
138
|
workflows = query.paginate(page=page, per_page=per_page, error_out=False)
|
|
134
|
-
|
|
139
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
140
|
+
workflows = query.all()
|
|
141
|
+
workflow_names = [w.name for w in workflows]
|
|
142
|
+
return jsonify({
|
|
143
|
+
"workflows": workflow_names,
|
|
144
|
+
})
|
|
145
|
+
else:
|
|
146
|
+
# return HTML
|
|
147
|
+
return render_template("experiment_database.html", workflows=workflows, deck_list=deck_list, deck_name=deck_name)
|
|
135
148
|
|
|
136
149
|
|
|
137
150
|
@database.route("/edit_run_name", methods=['POST'])
|
|
@@ -200,6 +213,12 @@ def list_workflows():
|
|
|
200
213
|
per_page = 10
|
|
201
214
|
|
|
202
215
|
workflows = query.paginate(page=page, per_page=per_page, error_out=False)
|
|
216
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
217
|
+
workflows = query.all()
|
|
218
|
+
workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
|
|
219
|
+
return jsonify({
|
|
220
|
+
"workflow_data": workflow_data,
|
|
221
|
+
})
|
|
203
222
|
return render_template('workflow_run_database.html', workflows=workflows)
|
|
204
223
|
|
|
205
224
|
|
|
@@ -208,20 +227,40 @@ def get_workflow_steps(workflow_id):
|
|
|
208
227
|
workflow = WorkflowRun.query.get_or_404(workflow_id)
|
|
209
228
|
steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
|
|
210
229
|
|
|
211
|
-
#
|
|
230
|
+
# Use full objects for template rendering
|
|
212
231
|
grouped = {
|
|
213
232
|
"prep": [],
|
|
214
233
|
"script": {},
|
|
215
234
|
"cleanup": [],
|
|
216
235
|
}
|
|
217
236
|
|
|
237
|
+
# Use dicts for JSON response
|
|
238
|
+
grouped_json = {
|
|
239
|
+
"prep": [],
|
|
240
|
+
"script": {},
|
|
241
|
+
"cleanup": [],
|
|
242
|
+
}
|
|
243
|
+
|
|
218
244
|
for step in steps:
|
|
245
|
+
step_dict = step.as_dict()
|
|
246
|
+
|
|
219
247
|
if step.phase == "prep":
|
|
220
248
|
grouped["prep"].append(step)
|
|
249
|
+
grouped_json["prep"].append(step_dict)
|
|
250
|
+
|
|
221
251
|
elif step.phase == "script":
|
|
222
252
|
grouped["script"].setdefault(step.repeat_index, []).append(step)
|
|
253
|
+
grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
|
|
254
|
+
|
|
223
255
|
elif step.phase == "cleanup" or step.method_name == "stop":
|
|
224
256
|
grouped["cleanup"].append(step)
|
|
257
|
+
grouped_json["cleanup"].append(step_dict)
|
|
258
|
+
|
|
259
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
260
|
+
return jsonify({
|
|
261
|
+
"workflow_info": workflow.as_dict(),
|
|
262
|
+
"steps": grouped_json,
|
|
263
|
+
})
|
|
225
264
|
|
|
226
265
|
return render_template("experiment_step_view.html", workflow=workflow, grouped=grouped)
|
|
227
266
|
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -15,7 +15,7 @@ from ivoryos.utils.client_proxy import create_function, export_to_python
|
|
|
15
15
|
from ivoryos.utils.global_config import GlobalConfig
|
|
16
16
|
from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
|
|
17
17
|
create_form_from_action, create_all_builtin_forms
|
|
18
|
-
from ivoryos.utils.db_models import Script
|
|
18
|
+
from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
|
|
19
19
|
from ivoryos.utils.script_runner import ScriptRunner
|
|
20
20
|
# from ivoryos.utils.utils import load_workflows
|
|
21
21
|
|
|
@@ -25,32 +25,45 @@ design = Blueprint('design', __name__, template_folder='templates/design')
|
|
|
25
25
|
global_config = GlobalConfig()
|
|
26
26
|
runner = ScriptRunner()
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
@socketio.on('abort_pending')
|
|
30
|
-
def handle_abort_pending():
|
|
28
|
+
def abort_pending():
|
|
31
29
|
runner.abort_pending()
|
|
32
30
|
socketio.emit('log', {'message': "aborted pending iterations"})
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
@socketio.on('abort_current')
|
|
36
|
-
def handle_abort_current():
|
|
32
|
+
def abort_current():
|
|
37
33
|
runner.stop_execution()
|
|
38
34
|
socketio.emit('log', {'message': "stopped next task"})
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
@socketio.on('pause')
|
|
42
|
-
def handle_pause():
|
|
36
|
+
def pause():
|
|
43
37
|
runner.retry = False
|
|
44
38
|
msg = runner.toggle_pause()
|
|
45
39
|
socketio.emit('log', {'message': msg})
|
|
40
|
+
return msg
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
def handle_pause():
|
|
42
|
+
def retry():
|
|
49
43
|
runner.retry = True
|
|
50
44
|
msg = runner.toggle_pause()
|
|
51
45
|
socketio.emit('log', {'message': msg})
|
|
52
46
|
|
|
53
47
|
|
|
48
|
+
# ---- Socket.IO Event Handlers ----
|
|
49
|
+
|
|
50
|
+
@socketio.on('abort_pending')
|
|
51
|
+
def handle_abort_pending():
|
|
52
|
+
abort_pending()
|
|
53
|
+
|
|
54
|
+
@socketio.on('abort_current')
|
|
55
|
+
def handle_abort_current():
|
|
56
|
+
abort_current()
|
|
57
|
+
|
|
58
|
+
@socketio.on('pause')
|
|
59
|
+
def handle_pause():
|
|
60
|
+
pause()
|
|
61
|
+
|
|
62
|
+
@socketio.on('retry')
|
|
63
|
+
def handle_retry():
|
|
64
|
+
retry()
|
|
65
|
+
|
|
66
|
+
|
|
54
67
|
@socketio.on('connect')
|
|
55
68
|
def handle_abort_action():
|
|
56
69
|
# Fetch log messages from local file
|
|
@@ -312,11 +325,18 @@ def experiment_run():
|
|
|
312
325
|
config_preview = []
|
|
313
326
|
config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
|
|
314
327
|
try:
|
|
315
|
-
|
|
316
|
-
|
|
328
|
+
# todo
|
|
329
|
+
exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
330
|
+
# exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
317
333
|
flash(e.__str__())
|
|
318
|
-
|
|
319
|
-
|
|
334
|
+
# handle api request
|
|
335
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
336
|
+
return jsonify({"error": e.__str__()})
|
|
337
|
+
else:
|
|
338
|
+
return redirect(url_for("design.experiment_builder"))
|
|
339
|
+
|
|
320
340
|
config_file = request.args.get("filename")
|
|
321
341
|
config = []
|
|
322
342
|
if config_file:
|
|
@@ -356,25 +376,41 @@ def experiment_run():
|
|
|
356
376
|
flash(f"This script is not compatible with current deck, import {script.deck}")
|
|
357
377
|
if request.method == "POST":
|
|
358
378
|
bo_args = None
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
379
|
+
compiled = False
|
|
380
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
381
|
+
payload_json = request.get_json()
|
|
382
|
+
compiled = True
|
|
383
|
+
if "kwargs" in payload_json:
|
|
384
|
+
config = payload_json["kwargs"]
|
|
385
|
+
elif "parameters" in payload_json:
|
|
386
|
+
bo_args = payload_json
|
|
387
|
+
repeat = payload_json.pop("repeat", None)
|
|
388
|
+
else:
|
|
389
|
+
if "bo" in request.form:
|
|
390
|
+
bo_args = request.form.to_dict()
|
|
391
|
+
if "online-config" in request.form:
|
|
392
|
+
config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
|
|
393
|
+
repeat = request.form.get('repeat', None)
|
|
365
394
|
|
|
366
395
|
try:
|
|
367
396
|
datapath = current_app.config["DATA_FOLDER"]
|
|
368
397
|
run_name = script.validate_function_name(run_name)
|
|
369
398
|
runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
|
|
370
399
|
logger=g.logger, socketio=g.socketio, repeat_count=repeat,
|
|
371
|
-
output_path=datapath,
|
|
400
|
+
output_path=datapath, compiled=compiled,
|
|
401
|
+
current_app=current_app._get_current_object()
|
|
372
402
|
)
|
|
373
403
|
if utils.check_config_duplicate(config):
|
|
374
404
|
flash(f"WARNING: Duplicate in config entries.")
|
|
375
405
|
except Exception as e:
|
|
376
|
-
|
|
377
|
-
|
|
406
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
407
|
+
return jsonify({"error": e.__str__()})
|
|
408
|
+
else:
|
|
409
|
+
flash(e)
|
|
410
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
411
|
+
return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
|
|
412
|
+
else:
|
|
413
|
+
return render_template('experiment_run.html', script=script.script_dict, filename=filename,
|
|
378
414
|
dot_py=exec_string, line_collection=line_collection,
|
|
379
415
|
return_list=return_list, config_list=config_list, config_file_list=config_file_list,
|
|
380
416
|
config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
|
|
@@ -629,6 +665,66 @@ def duplicate_action(id: int):
|
|
|
629
665
|
return redirect(back)
|
|
630
666
|
|
|
631
667
|
|
|
632
|
-
|
|
668
|
+
# ---- HTTP API Endpoints ----
|
|
669
|
+
|
|
670
|
+
@design.route("/api/status", methods=["GET"])
|
|
633
671
|
def runner_status():
|
|
634
|
-
|
|
672
|
+
runner_busy = global_config.runner_lock.locked()
|
|
673
|
+
status = {"busy": runner_busy}
|
|
674
|
+
task_status = global_config.runner_status
|
|
675
|
+
current_step = {}
|
|
676
|
+
# print(task_status)
|
|
677
|
+
if task_status is not None:
|
|
678
|
+
task_type = task_status["type"]
|
|
679
|
+
task_id = task_status["id"]
|
|
680
|
+
if task_type == "task":
|
|
681
|
+
step = SingleStep.query.get(task_id)
|
|
682
|
+
current_step = step.as_dict()
|
|
683
|
+
if task_type == "workflow":
|
|
684
|
+
workflow = WorkflowRun.query.get(task_id)
|
|
685
|
+
if workflow is not None:
|
|
686
|
+
latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(WorkflowStep.start_time.desc()).first()
|
|
687
|
+
if latest_step is not None:
|
|
688
|
+
current_step = latest_step.as_dict()
|
|
689
|
+
status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
|
|
690
|
+
status["current_task"] = current_step
|
|
691
|
+
return jsonify(status), 200
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@design.route("/api/abort_pending", methods=["POST"])
|
|
696
|
+
def api_abort_pending():
|
|
697
|
+
abort_pending()
|
|
698
|
+
return jsonify({"status": "ok"}), 200
|
|
699
|
+
|
|
700
|
+
@design.route("/api/abort_current", methods=["POST"])
|
|
701
|
+
def api_abort_current():
|
|
702
|
+
abort_current()
|
|
703
|
+
return jsonify({"status": "ok"}), 200
|
|
704
|
+
|
|
705
|
+
@design.route("/api/pause", methods=["POST"])
|
|
706
|
+
def api_pause():
|
|
707
|
+
msg = pause()
|
|
708
|
+
return jsonify({"status": "ok", "pause_status": msg}), 200
|
|
709
|
+
|
|
710
|
+
@design.route("/api/retry", methods=["POST"])
|
|
711
|
+
def api_retry():
|
|
712
|
+
retry()
|
|
713
|
+
return jsonify({"status": "ok, retrying failed step"}), 200
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
@design.route("/api/get_script", methods=["GET", "POST"])
|
|
717
|
+
def get_script():
|
|
718
|
+
script = utils.get_script_file()
|
|
719
|
+
script.sort_actions()
|
|
720
|
+
script_collection = script.compile()
|
|
721
|
+
if request.method == "POST":
|
|
722
|
+
# create a brand-new script
|
|
723
|
+
deck = global_config.deck
|
|
724
|
+
deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
|
|
725
|
+
script = Script(author=session.get('user'), deck=deck_name)
|
|
726
|
+
script_collection = request.get_json()
|
|
727
|
+
script.python_script = script_collection
|
|
728
|
+
utils.post_script_file(script)
|
|
729
|
+
return jsonify({"status": "ok"}), 200
|
|
730
|
+
return jsonify(script_collection), 200
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
</h2>
|
|
27
27
|
<div id="runpanel" class="accordion-collapse collapse show">
|
|
28
28
|
<div class="accordion-body">
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
<div class="row">
|
|
30
|
+
{% if script['script'] or script['prep'] or script['cleanup'] %}
|
|
31
31
|
<div class="col-lg-6 col-sm-12" id="run-panel" style="{{ 'display: none;' if pause_status else '' }}">
|
|
32
32
|
<ul class="nav nav-tabs" id="myTabs" role="tablist">
|
|
33
33
|
<li class="nav-item" role="presentation">
|
|
@@ -127,129 +127,62 @@
|
|
|
127
127
|
</form>
|
|
128
128
|
{% endif %}
|
|
129
129
|
</div>
|
|
130
|
-
|
|
131
|
-
{# <div class="tab-pane fade " id="tab3" role="tabpanel" aria-labelledby="tab3-tab">#}
|
|
132
|
-
{# <form role="form" method='POST' name="bo" action="{{ url_for('design.experiment_run')}}">#}
|
|
133
|
-
{# <div class="form-group">#}
|
|
134
|
-
{# <p><h5>Parameters:</h5><p>#}
|
|
135
|
-
{# {% for config in config_list %}#}
|
|
136
|
-
{# <div class="row g-3 align-items-center">#}
|
|
137
|
-
{# <div class="col-lg-3 col-sm-6 ">#}
|
|
138
|
-
{# {{config}}:#}
|
|
139
|
-
{# </div>#}
|
|
140
|
-
{# <div class="col-auto">#}
|
|
141
|
-
{# <label class="col-form-label" for="{{config}}_type">Type</label>#}
|
|
142
|
-
{# </div>#}
|
|
143
|
-
{# <div class="col-auto">#}
|
|
144
|
-
{# <select class="form-select" id="{{config}}_type" name="{{config}}_type">#}
|
|
145
|
-
{# <option selected value="range">range</option>#}
|
|
146
|
-
{# <option value="choice">choice</option>#}
|
|
147
|
-
{# <option value="fixed">fixed</option>#}
|
|
148
|
-
{# </select>#}
|
|
149
|
-
{# </div>#}
|
|
150
|
-
{# <div class="col-auto">#}
|
|
151
|
-
{# <label class="" for="{{config}}_value">Values</label>#}
|
|
152
|
-
{# </div>#}
|
|
153
|
-
{# <div class="col-auto">#}
|
|
154
|
-
{# <input type="text" class="form-control" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">#}
|
|
155
|
-
{# </div>#}
|
|
156
|
-
{# <div class="col-auto">#}
|
|
157
|
-
{# <input type="text" class="form-control" id="{{config}}_value_max" style="display: none;" placeholder="1, 2, 3">#}
|
|
158
|
-
{# </div>#}
|
|
159
|
-
{# </div>#}
|
|
160
|
-
{# {% endfor %}#}
|
|
161
|
-
{# <p><h5>Objective:</h5><p>#}
|
|
162
|
-
{# {% for objective in return_list %}#}
|
|
163
|
-
{# <div class="row gy-2 gx-3 align-items-center input-group">#}
|
|
164
|
-
{# <div class="col-3">#}
|
|
165
|
-
{# {{objective}}:#}
|
|
166
|
-
{# </div>#}
|
|
167
|
-
{# <div class="col-auto">#}
|
|
168
|
-
{# <label class="" for="{{objective}}_min">Minimize</label>#}
|
|
169
|
-
{# </div>#}
|
|
170
|
-
{# <div class="col-auto">#}
|
|
171
|
-
{# <select class="form-select" id="{{objective}}_min" name="{{objective}}_min">#}
|
|
172
|
-
{# <option selected>minimize</option>#}
|
|
173
|
-
{# <option>maximize</option>#}
|
|
174
|
-
{# <option>none</option>#}
|
|
175
|
-
{# </select>#}
|
|
176
|
-
{# </div>#}
|
|
177
|
-
{# <div class="col-auto">#}
|
|
178
|
-
{# <label class="" for="{{objective}}_threshold">Threshold</label>#}
|
|
179
|
-
{# </div>#}
|
|
180
|
-
{# <div class="col-auto">#}
|
|
181
|
-
{# <input type="text" class="form-control" id="{{objective}}_threshold" name="{{objective}}_threshold" placeholder="None">#}
|
|
182
|
-
{# </div>#}
|
|
183
|
-
{# </div>#}
|
|
184
|
-
{# {% endfor %}#}
|
|
185
|
-
{# <p><h5>Budget:</h5></p>#}
|
|
186
|
-
{# <div class="input-group mb-3">#}
|
|
187
|
-
{# <label class="input-group-text" for="repeat">Max iteration </label>#}
|
|
188
|
-
{# <input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">#}
|
|
189
|
-
{# </div>#}
|
|
190
|
-
{# {% if not no_deck_warning%}#}
|
|
191
|
-
{# <div class="input-group mb-3">#}
|
|
192
|
-
{# <button class="form-control" type="submit" name="bo">Run</button>#}
|
|
193
|
-
{# </div>#}
|
|
194
|
-
{# {% endif %}#}
|
|
195
|
-
{# </div>#}
|
|
196
|
-
{# </form>#}
|
|
197
|
-
{# </div>#}
|
|
198
|
-
<div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
|
|
199
|
-
<form method="POST" name="bo" action="{{ url_for('design.experiment_run') }}">
|
|
200
|
-
<div class="container py-2">
|
|
201
130
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<div class="row align-items-center mb-2">
|
|
206
|
-
<div class="col-3 col-form-label-sm">
|
|
207
|
-
{{ config }}:
|
|
208
|
-
</div>
|
|
209
|
-
<div class="col-6">
|
|
210
|
-
<select class="form-select form-select-sm" id="{{config}}_type" name="{{config}}_type">
|
|
211
|
-
<option selected value="range">range</option>
|
|
212
|
-
<option value="choice">choice</option>
|
|
213
|
-
<option value="fixed">fixed</option>
|
|
214
|
-
</select>
|
|
215
|
-
</div>
|
|
216
|
-
<div class="col-3">
|
|
217
|
-
<input type="text" class="form-control form-control-sm" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
|
|
218
|
-
</div>
|
|
219
|
-
</div>
|
|
220
|
-
{% endfor %}
|
|
131
|
+
<div class="tab-pane fade" id="tab3" role="tabpanel" aria-labelledby="tab3-tab">
|
|
132
|
+
<form method="POST" name="bo" action="{{ url_for('design.experiment_run') }}">
|
|
133
|
+
<div class="container py-2">
|
|
221
134
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
135
|
+
<!-- Parameters -->
|
|
136
|
+
<h6 class="fw-bold mt-2 mb-1">Parameters</h6>
|
|
137
|
+
{% for config in config_list %}
|
|
138
|
+
<div class="row align-items-center mb-2">
|
|
139
|
+
<div class="col-3 col-form-label-sm">
|
|
140
|
+
{{ config }}:
|
|
141
|
+
</div>
|
|
142
|
+
<div class="col-6">
|
|
143
|
+
<select class="form-select form-select-sm" id="{{config}}_type" name="{{config}}_type">
|
|
144
|
+
<option selected value="range">range</option>
|
|
145
|
+
<option value="choice">choice</option>
|
|
146
|
+
<option value="fixed">fixed</option>
|
|
147
|
+
</select>
|
|
148
|
+
</div>
|
|
149
|
+
<div class="col-3">
|
|
150
|
+
<input type="text" class="form-control form-control-sm" id="{{config}}_value" name="{{config}}_value" placeholder="1, 2, 3">
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
{% endfor %}
|
|
238
154
|
|
|
239
|
-
|
|
155
|
+
<!-- Objective -->
|
|
156
|
+
<h6 class="fw-bold mt-3 mb-1">Objectives</h6>
|
|
157
|
+
{% for objective in return_list %}
|
|
158
|
+
<div class="row align-items-center mb-2">
|
|
159
|
+
<div class="col-3 col-form-label-sm">
|
|
160
|
+
{{ objective }}:
|
|
161
|
+
</div>
|
|
162
|
+
<div class="col-6">
|
|
163
|
+
<select class="form-select form-select-sm" id="{{objective}}_min" name="{{objective}}_min">
|
|
164
|
+
<option selected>minimize</option>
|
|
165
|
+
<option>maximize</option>
|
|
166
|
+
<option>none</option>
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
{% endfor %}
|
|
240
171
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
</div>
|
|
172
|
+
<h6 class="fw-bold mt-3 mb-1">Budget</h6>
|
|
173
|
+
|
|
174
|
+
<div class="input-group mb-3">
|
|
175
|
+
<label class="input-group-text" for="repeat">Max iteration </label>
|
|
176
|
+
<input class="form-control" type="number" id="repeat" name="repeat" min="1" max="1000" value="25">
|
|
177
|
+
</div>
|
|
178
|
+
{% if not no_deck_warning%}
|
|
179
|
+
<div class="input-group mb-3">
|
|
180
|
+
<button class="form-control" type="submit" name="bo">Run</button>
|
|
181
|
+
</div>
|
|
182
|
+
{% endif %}
|
|
183
|
+
</div>
|
|
184
|
+
</form>
|
|
185
|
+
</div>
|
|
253
186
|
|
|
254
187
|
|
|
255
188
|
<div class="tab-pane fade {{ 'show active' if config_list and config_list|count<=5 else '' }}" id="tab4" role="tabpanel" aria-labelledby="tab4-tab">
|
|
@@ -278,6 +211,12 @@
|
|
|
278
211
|
</div>
|
|
279
212
|
</div>
|
|
280
213
|
</div>
|
|
214
|
+
{% else %}
|
|
215
|
+
<div class="col-lg-6 col-sm-12" id="placeholder-panel">
|
|
216
|
+
|
|
217
|
+
</div>
|
|
218
|
+
{% endif %}
|
|
219
|
+
|
|
281
220
|
<div class="col-lg-6 col-sm-12" id="code-panel" style="{{ '' if pause_status else 'display: none;'}}">
|
|
282
221
|
<p>
|
|
283
222
|
<h5>Progress:</h5>
|
|
@@ -299,7 +238,7 @@
|
|
|
299
238
|
{% endif %}
|
|
300
239
|
{% if "cleanup" in line_collection.keys() %}
|
|
301
240
|
{% set stype = "cleanup" %}
|
|
302
|
-
<h6>
|
|
241
|
+
<h6>Cleanup:</h6>
|
|
303
242
|
{% for code in line_collection["cleanup"] %}
|
|
304
243
|
<pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
|
|
305
244
|
{% endfor %}
|
|
@@ -337,7 +276,7 @@
|
|
|
337
276
|
<div id="logging-panel"></div>
|
|
338
277
|
</div>
|
|
339
278
|
</div>
|
|
340
|
-
|
|
279
|
+
|
|
341
280
|
</div>
|
|
342
281
|
</div>
|
|
343
282
|
</div>
|
|
@@ -10,21 +10,26 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
|
10
10
|
var progressBar = document.getElementById('progress-bar-inner');
|
|
11
11
|
progressBar.style.width = progress + '%';
|
|
12
12
|
progressBar.setAttribute('aria-valuenow', progress);
|
|
13
|
+
const runPanel = document.getElementById("run-panel");
|
|
14
|
+
const codePanel = document.getElementById("code-panel");
|
|
13
15
|
if (progress === 1) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if (runPanel) runPanel.style.display = "none";
|
|
17
|
+
if (codePanel) {
|
|
18
|
+
codePanel.style.display = "block";
|
|
19
|
+
codePanel.scrollIntoView({ behavior: "smooth" });
|
|
20
|
+
}
|
|
21
|
+
progressBar.classList.remove('bg-success');
|
|
22
|
+
progressBar.classList.remove('bg-danger');
|
|
23
|
+
progressBar.classList.add('progress-bar-animated');
|
|
19
24
|
}
|
|
20
25
|
if (progress === 100) {
|
|
21
26
|
// Remove animation and set green color when 100% is reached
|
|
22
27
|
progressBar.classList.remove('progress-bar-animated');
|
|
23
28
|
progressBar.classList.add('bg-success'); // Bootstrap class for green color
|
|
24
29
|
setTimeout(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
if (runPanel) runPanel.style.display = "block";
|
|
31
|
+
if (codePanel) codePanel.style.display = "none";
|
|
32
|
+
}, 1000); // Small delay to let users see the completion
|
|
28
33
|
}
|
|
29
34
|
});
|
|
30
35
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from ivoryos.utils.utils import install_and_import
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def ax_init_form(data, arg_types):
|
|
5
|
+
"""
|
|
6
|
+
create Ax campaign from the web form input
|
|
7
|
+
:param data:
|
|
8
|
+
"""
|
|
9
|
+
install_and_import("ax", "ax-platform")
|
|
10
|
+
parameter, objectives = ax_wrapper(data, arg_types)
|
|
11
|
+
from ax.service.ax_client import AxClient
|
|
12
|
+
ax_client = AxClient()
|
|
13
|
+
ax_client.create_experiment(parameter, objectives=objectives)
|
|
14
|
+
return ax_client
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ax_wrapper(data: dict, arg_types: list):
|
|
18
|
+
"""
|
|
19
|
+
Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
|
|
20
|
+
:param data: e.g.,
|
|
21
|
+
{
|
|
22
|
+
"param_1_type": "range", "param_1_value": [1,2],
|
|
23
|
+
"param_2_type": "range", "param_2_value": [1,2],
|
|
24
|
+
"obj_1_min": True,
|
|
25
|
+
"obj_2_min": True
|
|
26
|
+
}
|
|
27
|
+
:return: the optimization campaign parameters
|
|
28
|
+
parameter=[
|
|
29
|
+
{"name": "param_1", "type": "range", "bounds": [1,2]},
|
|
30
|
+
{"name": "param_1", "type": "range", "bounds": [1,2]}
|
|
31
|
+
]
|
|
32
|
+
objectives=[
|
|
33
|
+
{"name": "obj_1", "min": True, "threshold": None},
|
|
34
|
+
{"name": "obj_2", "min": True, "threshold": None},
|
|
35
|
+
]
|
|
36
|
+
"""
|
|
37
|
+
from ax.service.utils.instantiation import ObjectiveProperties
|
|
38
|
+
parameter = []
|
|
39
|
+
objectives = {}
|
|
40
|
+
# Iterate through the webui_data dictionary
|
|
41
|
+
for key, value in data.items():
|
|
42
|
+
# Check if the key corresponds to a parameter type
|
|
43
|
+
if "_type" in key:
|
|
44
|
+
param_name = key.split("_type")[0]
|
|
45
|
+
param_type = value
|
|
46
|
+
param_value = data[f"{param_name}_value"].split(",")
|
|
47
|
+
try:
|
|
48
|
+
values = [float(v) for v in param_value]
|
|
49
|
+
except Exception:
|
|
50
|
+
values = param_value
|
|
51
|
+
if param_type == "range":
|
|
52
|
+
param = {"name": param_name, "type": param_type, "bounds": values}
|
|
53
|
+
if param_type == "choice":
|
|
54
|
+
param = {"name": param_name, "type": param_type, "values": values}
|
|
55
|
+
if param_type == "fixed":
|
|
56
|
+
param = {"name": param_name, "type": param_type, "value": values[0]}
|
|
57
|
+
_type = arg_types[param_name] if arg_types[param_name] in ["str", "bool", "int"] else "float"
|
|
58
|
+
param.update({"value_type": _type})
|
|
59
|
+
parameter.append(param)
|
|
60
|
+
elif key.endswith("_min"):
|
|
61
|
+
if not value == 'none':
|
|
62
|
+
obj_name = key.split("_min")[0]
|
|
63
|
+
is_min = True if value == "minimize" else False
|
|
64
|
+
|
|
65
|
+
threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
|
|
66
|
+
properties = ObjectiveProperties(minimize=is_min)
|
|
67
|
+
objectives[obj_name] = properties
|
|
68
|
+
|
|
69
|
+
return parameter, objectives
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ax_init_opc(bo_args):
|
|
73
|
+
install_and_import("ax", "ax-platform")
|
|
74
|
+
from ax.service.ax_client import AxClient
|
|
75
|
+
from ax.service.utils.instantiation import ObjectiveProperties
|
|
76
|
+
|
|
77
|
+
ax_client = AxClient()
|
|
78
|
+
objectives = bo_args.get("objectives")
|
|
79
|
+
objectives_formatted = {}
|
|
80
|
+
for obj in objectives:
|
|
81
|
+
obj_name = obj.get("name")
|
|
82
|
+
minimize = obj.get("minimize")
|
|
83
|
+
objectives_formatted[obj_name] = ObjectiveProperties(minimize=minimize)
|
|
84
|
+
bo_args["objectives"] = objectives_formatted
|
|
85
|
+
ax_client.create_experiment(**bo_args)
|
|
86
|
+
|
|
87
|
+
return ax_client
|
ivoryos/utils/db_models.py
CHANGED
|
@@ -48,7 +48,8 @@ class Script(db.Model):
|
|
|
48
48
|
|
|
49
49
|
def __init__(self, name=None, deck=None, status=None, script_dict: dict = None, id_order: dict = None,
|
|
50
50
|
time_created=None, last_modified=None, editing_type=None, author: str = None,
|
|
51
|
-
registered:bool=False,
|
|
51
|
+
# registered:bool=False,
|
|
52
|
+
python_script: str = None
|
|
52
53
|
):
|
|
53
54
|
if script_dict is None:
|
|
54
55
|
script_dict = {"prep": [], "script": [], "cleanup": []}
|
|
@@ -76,6 +77,7 @@ class Script(db.Model):
|
|
|
76
77
|
self.id_order = id_order
|
|
77
78
|
self.editing_type = editing_type
|
|
78
79
|
self.author = author
|
|
80
|
+
self.python_script = python_script
|
|
79
81
|
# self.r = registered
|
|
80
82
|
|
|
81
83
|
def as_dict(self):
|
|
@@ -412,12 +414,13 @@ class Script(db.Model):
|
|
|
412
414
|
"""
|
|
413
415
|
line_collection = {}
|
|
414
416
|
for stype, func_str in exec_str_collection.items():
|
|
415
|
-
|
|
416
|
-
|
|
417
|
+
if func_str:
|
|
418
|
+
module = ast.parse(func_str)
|
|
419
|
+
func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
|
|
417
420
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
+
# Extract function body as source lines
|
|
422
|
+
line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
|
|
423
|
+
# print(line_collection[stype])
|
|
421
424
|
return line_collection
|
|
422
425
|
|
|
423
426
|
def compile(self, script_path=None):
|
|
@@ -670,7 +673,24 @@ class WorkflowStep(db.Model):
|
|
|
670
673
|
run_error = db.Column(db.Boolean, default=False)
|
|
671
674
|
|
|
672
675
|
def as_dict(self):
|
|
673
|
-
dict = self.__dict__
|
|
676
|
+
dict = self.__dict__.copy()
|
|
677
|
+
dict.pop('_sa_instance_state', None)
|
|
678
|
+
return dict
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class SingleStep(db.Model):
|
|
682
|
+
__tablename__ = 'single_steps'
|
|
683
|
+
|
|
684
|
+
id = db.Column(db.Integer, primary_key=True)
|
|
685
|
+
method_name = db.Column(db.String(128), nullable=False)
|
|
686
|
+
kwargs = db.Column(JSONType, nullable=False)
|
|
687
|
+
start_time = db.Column(db.DateTime)
|
|
688
|
+
end_time = db.Column(db.DateTime)
|
|
689
|
+
run_error = db.Column(db.String(128))
|
|
690
|
+
output = db.Column(JSONType)
|
|
691
|
+
|
|
692
|
+
def as_dict(self):
|
|
693
|
+
dict = self.__dict__.copy()
|
|
674
694
|
dict.pop('_sa_instance_state', None)
|
|
675
695
|
return dict
|
|
676
696
|
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import threading
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class GlobalConfig:
|
|
@@ -13,7 +13,8 @@ class GlobalConfig:
|
|
|
13
13
|
cls._instance._defined_variables = {}
|
|
14
14
|
cls._instance._api_variables = set()
|
|
15
15
|
cls._instance._deck_snapshot = {}
|
|
16
|
-
cls._instance.
|
|
16
|
+
cls._instance._runner_lock = threading.Lock()
|
|
17
|
+
cls._instance._runner_status = None
|
|
17
18
|
return cls._instance
|
|
18
19
|
|
|
19
20
|
@property
|
|
@@ -70,9 +71,17 @@ class GlobalConfig:
|
|
|
70
71
|
self._api_variables = value
|
|
71
72
|
|
|
72
73
|
@property
|
|
73
|
-
def
|
|
74
|
-
return self.
|
|
74
|
+
def runner_lock(self):
|
|
75
|
+
return self._runner_lock
|
|
75
76
|
|
|
76
|
-
@
|
|
77
|
-
def
|
|
78
|
-
self.
|
|
77
|
+
@runner_lock.setter
|
|
78
|
+
def runner_lock(self, value):
|
|
79
|
+
self._runner_lock = value
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def runner_status(self):
|
|
83
|
+
return self._runner_status
|
|
84
|
+
|
|
85
|
+
@runner_status.setter
|
|
86
|
+
def runner_status(self, value):
|
|
87
|
+
self._runner_status = value
|
ivoryos/utils/script_runner.py
CHANGED
|
@@ -5,8 +5,8 @@ import threading
|
|
|
5
5
|
import time
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
|
|
8
|
-
from ivoryos.utils import utils
|
|
9
|
-
from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db
|
|
8
|
+
from ivoryos.utils import utils, bo_campaign
|
|
9
|
+
from ivoryos.utils.db_models import Script, WorkflowRun, WorkflowStep, db, SingleStep
|
|
10
10
|
from ivoryos.utils.global_config import GlobalConfig
|
|
11
11
|
|
|
12
12
|
global_config = GlobalConfig()
|
|
@@ -26,8 +26,9 @@ class ScriptRunner:
|
|
|
26
26
|
self.stop_pending_event = threading.Event()
|
|
27
27
|
self.stop_current_event = threading.Event()
|
|
28
28
|
self.is_running = False
|
|
29
|
-
self.lock =
|
|
29
|
+
self.lock = global_config.runner_lock
|
|
30
30
|
self.paused = False
|
|
31
|
+
self.current_app = None
|
|
31
32
|
|
|
32
33
|
def toggle_pause(self):
|
|
33
34
|
"""Toggles between pausing and resuming the script"""
|
|
@@ -59,23 +60,29 @@ class ScriptRunner:
|
|
|
59
60
|
self.stop_current_event.set()
|
|
60
61
|
self.abort_pending()
|
|
61
62
|
|
|
63
|
+
|
|
62
64
|
def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
|
|
63
|
-
output_path="", current_app=None):
|
|
64
|
-
# Get run.id
|
|
65
|
+
output_path="", compiled=False, current_app=None):
|
|
65
66
|
global deck
|
|
66
67
|
if deck is None:
|
|
67
68
|
deck = global_config.deck
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
|
|
70
|
+
if self.current_app is None:
|
|
71
|
+
self.current_app = current_app
|
|
72
|
+
time.sleep(1) # Optional: may help ensure deck readiness
|
|
73
|
+
|
|
74
|
+
# Try to acquire lock without blocking
|
|
75
|
+
if not self.lock.acquire(blocking=False):
|
|
76
|
+
if logger:
|
|
71
77
|
logger.info("System is busy. Please wait for it to finish or stop it before starting a new one.")
|
|
72
|
-
|
|
73
|
-
self.is_running = True
|
|
78
|
+
return None
|
|
74
79
|
|
|
75
80
|
self.reset_stop_event()
|
|
76
81
|
|
|
77
|
-
thread = threading.Thread(
|
|
78
|
-
|
|
82
|
+
thread = threading.Thread(
|
|
83
|
+
target=self._run_with_stop_check,
|
|
84
|
+
args=(script, repeat_count, run_name, logger, socketio, config, bo_args, output_path, current_app, compiled)
|
|
85
|
+
)
|
|
79
86
|
thread.start()
|
|
80
87
|
return thread
|
|
81
88
|
|
|
@@ -86,7 +93,7 @@ class ScriptRunner:
|
|
|
86
93
|
:param kwargs: Arguments to pass to the function
|
|
87
94
|
:return: The final result of the function execution
|
|
88
95
|
"""
|
|
89
|
-
_func_str = script.compile()
|
|
96
|
+
_func_str = script.python_script or script.compile()
|
|
90
97
|
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
91
98
|
global deck
|
|
92
99
|
# global deck, registered_workflows
|
|
@@ -138,6 +145,8 @@ class ScriptRunner:
|
|
|
138
145
|
method_name=method_name,
|
|
139
146
|
start_time=start_time,
|
|
140
147
|
)
|
|
148
|
+
db.session.add(step)
|
|
149
|
+
db.session.commit()
|
|
141
150
|
logger.info(f"Executing: {line}")
|
|
142
151
|
socketio.emit('execution', {'section': f"{section_name}-{index}"})
|
|
143
152
|
# self._emit_progress(socketio, 100)
|
|
@@ -158,7 +167,7 @@ class ScriptRunner:
|
|
|
158
167
|
step.run_error = True
|
|
159
168
|
self.toggle_pause()
|
|
160
169
|
step.end_time = datetime.now()
|
|
161
|
-
db.session.add(step)
|
|
170
|
+
# db.session.add(step)
|
|
162
171
|
db.session.commit()
|
|
163
172
|
|
|
164
173
|
self.pause_event.wait()
|
|
@@ -173,7 +182,7 @@ class ScriptRunner:
|
|
|
173
182
|
return exec_locals # Return the 'results' variable
|
|
174
183
|
|
|
175
184
|
def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
|
|
176
|
-
output_path, current_app):
|
|
185
|
+
output_path, current_app, compiled):
|
|
177
186
|
time.sleep(1)
|
|
178
187
|
# _func_str = script.compile()
|
|
179
188
|
# step_list_dict: dict = script.convert_to_lines(_func_str)
|
|
@@ -183,11 +192,12 @@ class ScriptRunner:
|
|
|
183
192
|
script_dict = script.script_dict
|
|
184
193
|
with current_app.app_context():
|
|
185
194
|
|
|
186
|
-
run = WorkflowRun(name=script.name or "untitled", platform=script.deck,start_time=datetime.now())
|
|
195
|
+
run = WorkflowRun(name=script.name or "untitled", platform=script.deck or "deck",start_time=datetime.now())
|
|
187
196
|
db.session.add(run)
|
|
188
|
-
db.session.
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
db.session.commit()
|
|
198
|
+
run_id = run.id # Save the ID
|
|
199
|
+
global_config.runner_status = {"id":run_id, "type": "workflow"}
|
|
200
|
+
self._run_actions(script, section_name="prep", logger=logger, socketio=socketio, run_id=run_id)
|
|
191
201
|
output_list = []
|
|
192
202
|
_, arg_type = script.config("script")
|
|
193
203
|
_, return_list = script.config_return()
|
|
@@ -195,43 +205,45 @@ class ScriptRunner:
|
|
|
195
205
|
# Run "script" section multiple times
|
|
196
206
|
if repeat_count:
|
|
197
207
|
self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, script,
|
|
198
|
-
run_name, return_list, logger, socketio, run_id=
|
|
208
|
+
run_name, return_list, compiled, logger, socketio, run_id=run_id)
|
|
199
209
|
elif config:
|
|
200
210
|
self._run_config_section(config, arg_type, output_list, script, run_name, logger,
|
|
201
|
-
socketio, run_id=
|
|
211
|
+
socketio, run_id=run_id, compiled=compiled)
|
|
202
212
|
|
|
203
213
|
# Run "cleanup" section once
|
|
204
|
-
self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=
|
|
214
|
+
self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
|
|
205
215
|
# Reset the running flag when done
|
|
206
|
-
|
|
207
|
-
self.is_running = False
|
|
216
|
+
self.lock.release()
|
|
208
217
|
# Save results if necessary
|
|
209
218
|
filename = None
|
|
210
|
-
if output_list:
|
|
219
|
+
if not script.python_script and output_list:
|
|
211
220
|
filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
|
212
221
|
self._emit_progress(socketio, 100)
|
|
222
|
+
with current_app.app_context():
|
|
223
|
+
run = db.session.get(WorkflowRun, run_id) # SQLAlchemy 1.4+ recommended method
|
|
213
224
|
run.end_time = datetime.now()
|
|
214
225
|
run.data_path = filename
|
|
215
226
|
db.session.commit()
|
|
216
227
|
|
|
217
228
|
def _run_actions(self, script, section_name="", logger=None, socketio=None, run_id=None):
|
|
218
|
-
_func_str = script.compile()
|
|
229
|
+
_func_str = script.python_script or script.compile()
|
|
219
230
|
step_list: list = script.convert_to_lines(_func_str).get(section_name, [])
|
|
220
231
|
logger.info(f'Executing {section_name} steps') if step_list else logger.info(f'No {section_name} steps')
|
|
221
232
|
if self.stop_pending_event.is_set():
|
|
222
233
|
logger.info(f"Stopping execution during {section_name} section.")
|
|
223
234
|
return
|
|
224
|
-
|
|
235
|
+
if step_list:
|
|
236
|
+
self.exec_steps(script, section_name, logger, socketio, run_id=run_id, i_progress=0)
|
|
225
237
|
|
|
226
|
-
def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id):
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
def _run_config_section(self, config, arg_type, output_list, script, run_name, logger, socketio, run_id, compiled=True):
|
|
239
|
+
if not compiled:
|
|
240
|
+
for i in config:
|
|
241
|
+
try:
|
|
242
|
+
i = utils.convert_config_type(i, arg_type)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.info(e)
|
|
245
|
+
compiled = False
|
|
246
|
+
break
|
|
235
247
|
if compiled:
|
|
236
248
|
for i, kwargs in enumerate(config):
|
|
237
249
|
kwargs = dict(kwargs)
|
|
@@ -248,11 +260,14 @@ class ScriptRunner:
|
|
|
248
260
|
# kwargs.update(output)
|
|
249
261
|
output_list.append(output)
|
|
250
262
|
|
|
251
|
-
def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list,
|
|
263
|
+
def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, script, run_name, return_list, compiled,
|
|
252
264
|
logger, socketio, run_id):
|
|
253
265
|
if bo_args:
|
|
254
266
|
logger.info('Initializing optimizer...')
|
|
255
|
-
|
|
267
|
+
if compiled:
|
|
268
|
+
ax_client = bo_campaign.ax_init_opc(bo_args)
|
|
269
|
+
else:
|
|
270
|
+
ax_client = bo_campaign.ax_init_form(bo_args, arg_types)
|
|
256
271
|
for i_progress in range(int(repeat_count)):
|
|
257
272
|
if self.stop_pending_event.is_set():
|
|
258
273
|
logger.info(f'Stopping execution during {run_name}: {i_progress + 1}/{int(repeat_count)}')
|
|
@@ -311,9 +326,9 @@ class ScriptRunner:
|
|
|
311
326
|
|
|
312
327
|
def get_status(self):
|
|
313
328
|
"""Returns current status of the script runner."""
|
|
314
|
-
with self.
|
|
329
|
+
with self.current_app.app_context():
|
|
315
330
|
return {
|
|
316
|
-
"is_running": self.
|
|
331
|
+
"is_running": self.lock.locked(),
|
|
317
332
|
"paused": self.paused,
|
|
318
333
|
"stop_pending": self.stop_pending_event.is_set(),
|
|
319
334
|
"stop_current": self.stop_current_event.is_set(),
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from ivoryos.utils.db_models import db, SingleStep
|
|
6
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
7
|
+
|
|
8
|
+
global_config = GlobalConfig()
|
|
9
|
+
global deck
|
|
10
|
+
deck = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TaskRunner:
|
|
14
|
+
def __init__(self, globals_dict=None):
|
|
15
|
+
self.retry = False
|
|
16
|
+
if globals_dict is None:
|
|
17
|
+
globals_dict = globals()
|
|
18
|
+
self.globals_dict = globals_dict
|
|
19
|
+
self.lock = global_config.runner_lock
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
|
|
23
|
+
global deck
|
|
24
|
+
if deck is None:
|
|
25
|
+
deck = global_config.deck
|
|
26
|
+
|
|
27
|
+
# Try to acquire lock without blocking
|
|
28
|
+
if not self.lock.acquire(blocking=False):
|
|
29
|
+
current_status = global_config.runner_status
|
|
30
|
+
current_status["status"] = "busy"
|
|
31
|
+
return current_status
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if wait:
|
|
35
|
+
output = self._run_single_step(component, method, kwargs, current_app)
|
|
36
|
+
else:
|
|
37
|
+
print("running with thread")
|
|
38
|
+
thread = threading.Thread(
|
|
39
|
+
target=self._run_single_step, args=(component, method, kwargs, current_app)
|
|
40
|
+
)
|
|
41
|
+
thread.start()
|
|
42
|
+
time.sleep(0.1)
|
|
43
|
+
output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
|
|
44
|
+
|
|
45
|
+
return output
|
|
46
|
+
|
|
47
|
+
def _get_executable(self, component, deck, method):
|
|
48
|
+
if component.startswith("deck."):
|
|
49
|
+
component = component.split(".")[1]
|
|
50
|
+
instrument = getattr(deck, component)
|
|
51
|
+
else:
|
|
52
|
+
temp_connections = global_config.defined_variables
|
|
53
|
+
instrument = temp_connections.get(component)
|
|
54
|
+
function_executable = getattr(instrument, method)
|
|
55
|
+
return function_executable
|
|
56
|
+
|
|
57
|
+
def _run_single_step(self, component, method, kwargs, current_app=None):
|
|
58
|
+
try:
|
|
59
|
+
function_executable = self._get_executable(component, deck, method)
|
|
60
|
+
method_name = f"{function_executable.__self__.__class__.__name__}.{function_executable.__name__}"
|
|
61
|
+
except Exception as e:
|
|
62
|
+
self.lock.release()
|
|
63
|
+
return {"status": "error", "msg": e.__str__()}
|
|
64
|
+
|
|
65
|
+
# with self.lock:
|
|
66
|
+
with current_app.app_context():
|
|
67
|
+
step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=False, start_time=datetime.now())
|
|
68
|
+
db.session.add(step)
|
|
69
|
+
db.session.commit()
|
|
70
|
+
global_config.runner_status = {"id":step.id, "type": "task"}
|
|
71
|
+
try:
|
|
72
|
+
output = function_executable(**kwargs)
|
|
73
|
+
step.output = output
|
|
74
|
+
step.end_time = datetime.now()
|
|
75
|
+
except Exception as e:
|
|
76
|
+
step.run_error = e.__str__()
|
|
77
|
+
step.end_time = datetime.now()
|
|
78
|
+
finally:
|
|
79
|
+
db.session.commit()
|
|
80
|
+
self.lock.release()
|
|
81
|
+
return output
|
ivoryos/utils/utils.py
CHANGED
|
@@ -240,74 +240,6 @@ def start_logger(socketio: SocketIO, logger_name: str, log_filename: str = None)
|
|
|
240
240
|
return logger
|
|
241
241
|
|
|
242
242
|
|
|
243
|
-
def ax_wrapper(data: dict, arg_types: list):
|
|
244
|
-
"""
|
|
245
|
-
Ax platform wrapper function for creating optimization campaign parameters and objective from the web form input
|
|
246
|
-
:param data: e.g.,
|
|
247
|
-
{
|
|
248
|
-
"param_1_type": "range", "param_1_value": [1,2],
|
|
249
|
-
"param_2_type": "range", "param_2_value": [1,2],
|
|
250
|
-
"obj_1_min": True,
|
|
251
|
-
"obj_2_min": True
|
|
252
|
-
}
|
|
253
|
-
:return: the optimization campaign parameters
|
|
254
|
-
parameter=[
|
|
255
|
-
{"name": "param_1", "type": "range", "bounds": [1,2]},
|
|
256
|
-
{"name": "param_1", "type": "range", "bounds": [1,2]}
|
|
257
|
-
]
|
|
258
|
-
objectives=[
|
|
259
|
-
{"name": "obj_1", "min": True, "threshold": None},
|
|
260
|
-
{"name": "obj_2", "min": True, "threshold": None},
|
|
261
|
-
]
|
|
262
|
-
"""
|
|
263
|
-
from ax.service.utils.instantiation import ObjectiveProperties
|
|
264
|
-
parameter = []
|
|
265
|
-
objectives = {}
|
|
266
|
-
# Iterate through the webui_data dictionary
|
|
267
|
-
for key, value in data.items():
|
|
268
|
-
# Check if the key corresponds to a parameter type
|
|
269
|
-
if "_type" in key:
|
|
270
|
-
param_name = key.split("_type")[0]
|
|
271
|
-
param_type = value
|
|
272
|
-
param_value = data[f"{param_name}_value"].split(",")
|
|
273
|
-
try:
|
|
274
|
-
values = [float(v) for v in param_value]
|
|
275
|
-
except Exception:
|
|
276
|
-
values = param_value
|
|
277
|
-
if param_type == "range":
|
|
278
|
-
param = {"name": param_name, "type": param_type, "bounds": values}
|
|
279
|
-
if param_type == "choice":
|
|
280
|
-
param = {"name": param_name, "type": param_type, "values": values}
|
|
281
|
-
if param_type == "fixed":
|
|
282
|
-
param = {"name": param_name, "type": param_type, "value": values[0]}
|
|
283
|
-
_type = arg_types[param_name] if arg_types[param_name] in ["str", "bool", "int"] else "float"
|
|
284
|
-
param.update({"value_type": _type})
|
|
285
|
-
parameter.append(param)
|
|
286
|
-
elif key.endswith("_min"):
|
|
287
|
-
if not value == 'none':
|
|
288
|
-
obj_name = key.split("_min")[0]
|
|
289
|
-
is_min = True if value == "minimize" else False
|
|
290
|
-
|
|
291
|
-
threshold = None if f"{obj_name}_threshold" not in data else data[f"{obj_name}_threshold"]
|
|
292
|
-
properties = ObjectiveProperties(minimize=is_min)
|
|
293
|
-
objectives[obj_name] = properties
|
|
294
|
-
|
|
295
|
-
return parameter, objectives
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def ax_initiation(data, arg_types):
|
|
299
|
-
"""
|
|
300
|
-
create Ax campaign from the web form input
|
|
301
|
-
:param data:
|
|
302
|
-
"""
|
|
303
|
-
install_and_import("ax", "ax-platform")
|
|
304
|
-
parameter, objectives = ax_wrapper(data, arg_types)
|
|
305
|
-
from ax.service.ax_client import AxClient
|
|
306
|
-
ax_client = AxClient()
|
|
307
|
-
ax_client.create_experiment(parameter, objectives=objectives)
|
|
308
|
-
return ax_client
|
|
309
|
-
|
|
310
|
-
|
|
311
243
|
def get_arg_type(args, parameters):
|
|
312
244
|
"""get argument type from signature"""
|
|
313
245
|
arg_types = {}
|
ivoryos/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.0.
|
|
1
|
+
__version__ = "1.0.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ivoryos
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
|
|
5
5
|
Home-page: https://gitlab.com/heingroup/ivoryos
|
|
6
6
|
Author: Ivory Zhang
|
|
@@ -22,8 +22,8 @@ Requires-Dist: python-dotenv
|
|
|
22
22
|
[](https://pypi.org/project/ivoryos/)
|
|
23
23
|

|
|
24
24
|
[](https://youtu.be/dFfJv9I2-1g)
|
|
25
|
-
[](https://www.nature.com/articles/s41467-025-60514-w)
|
|
26
|
+
[](https://discord.gg/AX5P9EdGVX)
|
|
27
27
|
|
|
28
28
|

|
|
29
29
|
# ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
ivoryos/__init__.py,sha256=1v6LwuIao8WbmSskifzdfde7E_gH3PWCYaNpLaRtYZk,7341
|
|
2
2
|
ivoryos/config.py,sha256=3FPBYTIBhQTKDvsEoR8ZeTmg65D-CSFEdGmOuIL4pSI,1311
|
|
3
|
-
ivoryos/version.py,sha256=
|
|
3
|
+
ivoryos/version.py,sha256=d4QHYmS_30j0hPN8NmNPnQ_Z0TphDRbu4MtQj9cT9e8,22
|
|
4
4
|
ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
ivoryos/routes/auth/auth.py,sha256=7CdXjGAr1B_xsmwweakTWOoROgsOJf0MNTzlMP_5Nus,3240
|
|
7
7
|
ivoryos/routes/auth/templates/auth/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
|
|
8
8
|
ivoryos/routes/auth/templates/auth/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
|
|
9
9
|
ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
ivoryos/routes/control/control.py,sha256=
|
|
10
|
+
ivoryos/routes/control/control.py,sha256=AbJA1dwWAWZi3eGnsbNObPASVsN7pZbL6la25xB52hI,14209
|
|
11
11
|
ivoryos/routes/control/templates/control/controllers.html,sha256=iIp0h6WA68gQj9OsoiB7dU1BqH8CGomTueR73F4C8eY,4274
|
|
12
12
|
ivoryos/routes/control/templates/control/controllers_home.html,sha256=VQ77HRvBlyBrQ3al5fcKF5Y6_vKtU8WeAhilqQQltAo,2997
|
|
13
13
|
ivoryos/routes/control/templates/control/controllers_new.html,sha256=uOQo9kYmwX2jk3KZDkMUF_ylfNUIs_oIWb_kk_MMVDM,4921
|
|
14
14
|
ivoryos/routes/database/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
ivoryos/routes/database/database.py,sha256=
|
|
15
|
+
ivoryos/routes/database/database.py,sha256=97387fF6tQIfb-BF6krILRP4a4_8jOJmGcE7BoPgjmk,9127
|
|
16
16
|
ivoryos/routes/database/templates/database/experiment_database.html,sha256=edlCcKfrS91gGG1dPFQjC9xD7F7nWNNqS3S6Oa7apzs,3460
|
|
17
17
|
ivoryos/routes/database/templates/database/experiment_step_view.html,sha256=u8_XYhiZ98PzglMzFEkuM1Tk9hVWf79xXIrpHVDxKa0,3618
|
|
18
18
|
ivoryos/routes/database/templates/database/step_card.html,sha256=F4JRfacrEQfk2rrEbcI_i7G84nzKKDmCrMSmStLb4W4,290
|
|
19
19
|
ivoryos/routes/database/templates/database/workflow_run_database.html,sha256=MczK9my9u0SyQsMFLbc6CXeZqKaBo5vk1SpwjkcZdqk,3571
|
|
20
20
|
ivoryos/routes/design/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
ivoryos/routes/design/design.py,sha256=
|
|
21
|
+
ivoryos/routes/design/design.py,sha256=ameAcgTLlrfOENUa48xxp64psuh2aZ0JuVfIj7zj-LM,28968
|
|
22
22
|
ivoryos/routes/design/templates/design/experiment_builder.html,sha256=rEdcHj5onJG_4MejdFBPnJVzsvCMp1KDteqNkpx24kQ,29430
|
|
23
|
-
ivoryos/routes/design/templates/design/experiment_run.html,sha256=
|
|
23
|
+
ivoryos/routes/design/templates/design/experiment_run.html,sha256=7VP0Vo98phcYnFennd5vqaMK1M1QBwDmM-b9aZb8jOw,26282
|
|
24
24
|
ivoryos/routes/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
ivoryos/routes/main/main.py,sha256=yuVJzXAob1kc1dfflkTBIZQ0tdf6kChfuq-uQlN1e9Q,957
|
|
26
26
|
ivoryos/routes/main/templates/main/help.html,sha256=IOktMEsOPk0SCiMBXZ4mpffClERAyX8W82fel71M3M0,9370
|
|
@@ -31,20 +31,22 @@ ivoryos/static/style.css,sha256=zQVx35A5g6JMJ-K84-6fSKtzXGjp_p5ZVG6KLHPM2IE,4021
|
|
|
31
31
|
ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
|
|
32
32
|
ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
|
|
33
33
|
ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
|
|
34
|
-
ivoryos/static/js/socket_handler.js,sha256=
|
|
34
|
+
ivoryos/static/js/socket_handler.js,sha256=2Iyv_3METjhSlSavs_L9FE3PKY4xDEpfzJpd2FywY9o,5300
|
|
35
35
|
ivoryos/static/js/sortable_card.js,sha256=ifmlGe3yy0U_KzMphV4ClRhK2DLOvkELYMlq1vECuac,807
|
|
36
36
|
ivoryos/static/js/sortable_design.js,sha256=wwpKfIzZGDxfX3moNz0cvPvm9YyHmopZK3wmkUdnBiw,4333
|
|
37
37
|
ivoryos/templates/base.html,sha256=sDdwqOIUP2Get-py4E59PkieoGWLFpX6wAJe93s4aRo,8518
|
|
38
38
|
ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
+
ivoryos/utils/bo_campaign.py,sha256=CVs7q15Pm2SRuJNaCvZKIxOFuv1xibM2yymtpAMAWOk,3285
|
|
39
40
|
ivoryos/utils/client_proxy.py,sha256=AzcSQGMqeCqVULP1a7vEKNe135NZYryVX63ke0wgK04,2099
|
|
40
|
-
ivoryos/utils/db_models.py,sha256=
|
|
41
|
+
ivoryos/utils/db_models.py,sha256=zlmmD2600CYyn79gQq8k0Vra7BDBKJBAyNLYclIWdvs,27382
|
|
41
42
|
ivoryos/utils/form.py,sha256=b3JKxRc1jN45-bXyfzSJT1lcssUuxT86FhRmNUDv5-U,20973
|
|
42
|
-
ivoryos/utils/global_config.py,sha256=
|
|
43
|
+
ivoryos/utils/global_config.py,sha256=OqfDrPgOzRdIUMD4V3pA9t6b-BATMjGZl8Jn7nkI56k,2138
|
|
43
44
|
ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
|
|
44
|
-
ivoryos/utils/script_runner.py,sha256=
|
|
45
|
-
ivoryos/utils/
|
|
46
|
-
ivoryos
|
|
47
|
-
ivoryos-1.0.
|
|
48
|
-
ivoryos-1.0.
|
|
49
|
-
ivoryos-1.0.
|
|
50
|
-
ivoryos-1.0.
|
|
45
|
+
ivoryos/utils/script_runner.py,sha256=k7bH9AeFGhx2tns-81q0JZFLCM9cvXCpZF7bH80RVzo,14504
|
|
46
|
+
ivoryos/utils/task_runner.py,sha256=u4nF0wOADu_HVlGYVTOXnUm1woWGgYAccr-ZCzgtb6Q,2899
|
|
47
|
+
ivoryos/utils/utils.py,sha256=OBwrRu02yh7pqG_lyl10zWr_RYes3xhMporxIz8lGYI,13579
|
|
48
|
+
ivoryos-1.0.1.dist-info/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
|
|
49
|
+
ivoryos-1.0.1.dist-info/METADATA,sha256=KEh9s448whM6VP-Knc5UUjl7DpoME989pKBfwLxFcHM,6992
|
|
50
|
+
ivoryos-1.0.1.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
|
|
51
|
+
ivoryos-1.0.1.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
|
|
52
|
+
ivoryos-1.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|