ivoryos 1.3.3__py3-none-any.whl → 1.3.5__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/__init__.py +6 -2
- ivoryos/app.py +2 -1
- ivoryos/routes/control/control.py +2 -2
- ivoryos/routes/design/design.py +9 -3
- ivoryos/routes/design/design_step.py +31 -10
- ivoryos/routes/design/templates/components/canvas_main.html +6 -1
- ivoryos/server.py +29 -22
- ivoryos/static/js/action_handlers.js +20 -0
- ivoryos/utils/db_models.py +23 -11
- ivoryos/utils/global_config.py +11 -0
- ivoryos/utils/py_to_json.py +19 -4
- ivoryos/utils/script_runner.py +36 -7
- ivoryos/utils/task_runner.py +30 -18
- ivoryos/utils/utils.py +3 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5.dist-info}/METADATA +41 -6
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5.dist-info}/RECORD +20 -20
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5.dist-info}/WHEEL +0 -0
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5.dist-info}/licenses/LICENSE +0 -0
- {ivoryos-1.3.3.dist-info → ivoryos-1.3.5.dist-info}/top_level.txt +0 -0
ivoryos/__init__.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
from ivoryos.server import run
|
|
1
|
+
from ivoryos.server import run, global_config
|
|
2
2
|
from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
|
|
3
3
|
from ivoryos.version import __version__ as ivoryos_version
|
|
4
4
|
from ivoryos.utils.decorators import block, BUILDING_BLOCKS
|
|
5
|
-
from ivoryos.app import app
|
|
5
|
+
from ivoryos.app import app, create_app, socketio, db
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
8
|
"block",
|
|
@@ -11,4 +11,8 @@ __all__ = [
|
|
|
11
11
|
"run",
|
|
12
12
|
"app",
|
|
13
13
|
"ivoryos_version",
|
|
14
|
+
"create_app",
|
|
15
|
+
"socketio",
|
|
16
|
+
"global_config",
|
|
17
|
+
"db"
|
|
14
18
|
]
|
ivoryos/app.py
CHANGED
|
@@ -41,7 +41,8 @@ def reset_old_schema(engine, db_dir):
|
|
|
41
41
|
old_workflow_run = 'workflow_runs' in tables
|
|
42
42
|
old_workflow_step = 'workflow_steps' in tables
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# v1.3.4 only delete and backup when there is runs but no phases
|
|
45
|
+
if not has_workflow_phase and old_workflow_run:
|
|
45
46
|
print("⚠️ Old workflow database detected! All previous workflows have been reset to support the new schema.")
|
|
46
47
|
# Backup old DB
|
|
47
48
|
db_path = os.path.join(db_dir, "ivoryos.db")
|
|
@@ -23,7 +23,7 @@ control.register_blueprint(control_temp)
|
|
|
23
23
|
@control.route("/", strict_slashes=False, methods=["GET", "POST"])
|
|
24
24
|
@control.route("/<string:instrument>", strict_slashes=False, methods=["GET", "POST"])
|
|
25
25
|
@login_required
|
|
26
|
-
def deck_controllers(instrument: str = None):
|
|
26
|
+
async def deck_controllers(instrument: str = None):
|
|
27
27
|
"""
|
|
28
28
|
.. :quickref: Direct Control; device (instruments) and methods
|
|
29
29
|
|
|
@@ -82,7 +82,7 @@ def deck_controllers(instrument: str = None):
|
|
|
82
82
|
|
|
83
83
|
wait = str(payload.get("hidden_wait", "true")).lower() == "true"
|
|
84
84
|
|
|
85
|
-
output = runner.run_single_step(
|
|
85
|
+
output = await runner.run_single_step(
|
|
86
86
|
component=instrument, method=method_name, kwargs=kwargs, wait=wait,
|
|
87
87
|
current_app=current_app._get_current_object()
|
|
88
88
|
)
|
ivoryos/routes/design/design.py
CHANGED
|
@@ -87,8 +87,11 @@ def experiment_builder():
|
|
|
87
87
|
|
|
88
88
|
# edit_action_info = session.get("edit_action")
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
try:
|
|
91
|
+
exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
92
|
+
except Exception as e:
|
|
93
|
+
exec_string = {}
|
|
94
|
+
flash(f"Error in Python script: {e}")
|
|
92
95
|
session['python_code'] = exec_string
|
|
93
96
|
|
|
94
97
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
@@ -316,6 +319,7 @@ def methods_handler(instrument: str = ''):
|
|
|
316
319
|
msg = ""
|
|
317
320
|
request.form
|
|
318
321
|
if "hidden_name" in request.form:
|
|
322
|
+
deck_snapshot = global_config.deck_snapshot
|
|
319
323
|
method_name = request.form.get("hidden_name", None)
|
|
320
324
|
form = forms.get(method_name) if forms else None
|
|
321
325
|
insert_position = request.form.get("drop_target_id", None)
|
|
@@ -334,7 +338,9 @@ def methods_handler(instrument: str = ''):
|
|
|
334
338
|
action = {"instrument": instrument, "action": function_name,
|
|
335
339
|
"args": kwargs,
|
|
336
340
|
"return": save_data,
|
|
337
|
-
'arg_types': primitive_arg_types
|
|
341
|
+
'arg_types': primitive_arg_types,
|
|
342
|
+
"coroutine": deck_snapshot[instrument][function_name].get("coroutine", False) if deck_snapshot else False,
|
|
343
|
+
}
|
|
338
344
|
script.add_action(action=action, insert_position=insert_position)
|
|
339
345
|
else:
|
|
340
346
|
msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
|
|
@@ -47,6 +47,7 @@ def save_step(uuid: int):
|
|
|
47
47
|
"""
|
|
48
48
|
script = utils.get_script_file()
|
|
49
49
|
action = script.find_by_uuid(uuid)
|
|
50
|
+
warning = None
|
|
50
51
|
if action is not None:
|
|
51
52
|
forms = create_form_from_action(action, script=script)
|
|
52
53
|
kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
|
|
@@ -55,14 +56,19 @@ def save_step(uuid: int):
|
|
|
55
56
|
kwargs = script.validate_variables(kwargs)
|
|
56
57
|
script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
|
|
57
58
|
else:
|
|
58
|
-
|
|
59
|
+
warning = f"Compilation failed: {str(forms.errors)}"
|
|
59
60
|
utils.post_script_file(script)
|
|
60
|
-
|
|
61
|
+
try:
|
|
62
|
+
exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
63
|
+
except Exception as e:
|
|
64
|
+
exec_string = {}
|
|
65
|
+
warning = f"Compilation failed: {str(e)}"
|
|
61
66
|
session['python_code'] = exec_string
|
|
62
67
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
63
68
|
return render_template("components/canvas_main.html",
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
script=script,
|
|
70
|
+
buttons_dict=design_buttons,
|
|
71
|
+
warning=warning)
|
|
66
72
|
|
|
67
73
|
@steps.delete("/draft/steps/<int:uuid>")
|
|
68
74
|
def delete_step(uuid: int):
|
|
@@ -82,12 +88,17 @@ def delete_step(uuid: int):
|
|
|
82
88
|
if request.method == 'DELETE':
|
|
83
89
|
script.delete_action(uuid)
|
|
84
90
|
utils.post_script_file(script)
|
|
85
|
-
|
|
91
|
+
warning = None
|
|
92
|
+
try:
|
|
93
|
+
exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
94
|
+
except Exception as e:
|
|
95
|
+
exec_string = {}
|
|
96
|
+
warning = f"Compilation failed: {str(e)}"
|
|
86
97
|
session['python_code'] = exec_string
|
|
87
98
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
88
99
|
return render_template("components/canvas_main.html",
|
|
89
100
|
script=script,
|
|
90
|
-
buttons_dict=design_buttons)
|
|
101
|
+
buttons_dict=design_buttons, warning=warning)
|
|
91
102
|
|
|
92
103
|
|
|
93
104
|
@steps.route("/draft/steps/<int:uuid>/duplicate", methods=["POST"], strict_slashes=False,)
|
|
@@ -107,13 +118,18 @@ def duplicate_action(uuid: int):
|
|
|
107
118
|
script = utils.get_script_file()
|
|
108
119
|
script.duplicate_action(uuid)
|
|
109
120
|
utils.post_script_file(script)
|
|
110
|
-
|
|
121
|
+
warning = None
|
|
122
|
+
try:
|
|
123
|
+
exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
124
|
+
except Exception as e:
|
|
125
|
+
exec_string = {}
|
|
126
|
+
warning = f"Compilation failed: {str(e)}"
|
|
111
127
|
session['python_code'] = exec_string
|
|
112
128
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
113
129
|
|
|
114
130
|
return render_template("components/canvas_main.html",
|
|
115
131
|
script=script,
|
|
116
|
-
buttons_dict=design_buttons)
|
|
132
|
+
buttons_dict=design_buttons, warning=warning)
|
|
117
133
|
|
|
118
134
|
|
|
119
135
|
@steps.route("/draft/steps/order", methods=['POST'])
|
|
@@ -133,13 +149,18 @@ def update_list():
|
|
|
133
149
|
script = utils.get_script_file()
|
|
134
150
|
script.currently_editing_order = order.split(",", len(script.currently_editing_script))
|
|
135
151
|
script.sort_actions()
|
|
152
|
+
warning = None
|
|
136
153
|
|
|
137
154
|
utils.post_script_file(script)
|
|
138
|
-
|
|
155
|
+
try:
|
|
156
|
+
exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
|
|
157
|
+
except Exception as e:
|
|
158
|
+
exec_string = {}
|
|
159
|
+
warning = f"Compilation failed: {str(e)}"
|
|
139
160
|
session['python_code'] = exec_string
|
|
140
161
|
|
|
141
162
|
# Return the updated canvas HTML instead of JSON
|
|
142
163
|
design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
|
|
143
164
|
return render_template("components/canvas_main.html",
|
|
144
165
|
script=script,
|
|
145
|
-
buttons_dict=design_buttons)
|
|
166
|
+
buttons_dict=design_buttons, warning=warning)
|
ivoryos/server.py
CHANGED
|
@@ -49,6 +49,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
49
49
|
enable_design: bool = True,
|
|
50
50
|
blueprint_plugins: Union[list, Blueprint] = [],
|
|
51
51
|
exclude_names: list = [],
|
|
52
|
+
notification_handler=None,
|
|
52
53
|
):
|
|
53
54
|
"""
|
|
54
55
|
Start ivoryOS app server.
|
|
@@ -65,6 +66,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
65
66
|
:param enable_design: enable design canvas, database and workflow execution
|
|
66
67
|
:param blueprint_plugins: Union[list[Blueprint], Blueprint] custom Blueprint pages
|
|
67
68
|
:param exclude_names: list[str] module names to exclude from parsing
|
|
69
|
+
:param notification_handler: notification handler function
|
|
68
70
|
"""
|
|
69
71
|
app = create_app(config_class=config or get_config()) # Create app instance using factory function
|
|
70
72
|
|
|
@@ -113,11 +115,33 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
113
115
|
output_path=app.config["OUTPUT_FOLDER"] if module is not None else None)
|
|
114
116
|
else:
|
|
115
117
|
app.config["ENABLE_LLM"] = False
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# --- Logger registration ---
|
|
121
|
+
if logger:
|
|
122
|
+
if isinstance(logger, str):
|
|
123
|
+
logger = [logger] # convert single logger to list
|
|
124
|
+
elif not isinstance(logger, list):
|
|
125
|
+
raise TypeError("logger must be a string or a list of strings.")
|
|
126
|
+
|
|
127
|
+
for log_name in logger:
|
|
128
|
+
utils.start_logger(socketio, log_filename=logger_path, logger_name=log_name)
|
|
129
|
+
|
|
130
|
+
# --- Notification handler registration ---
|
|
131
|
+
if notification_handler:
|
|
132
|
+
|
|
133
|
+
# make it a list if a single function is passed
|
|
134
|
+
if callable(notification_handler):
|
|
135
|
+
notification_handler = [notification_handler]
|
|
136
|
+
|
|
137
|
+
if not isinstance(notification_handler, list):
|
|
138
|
+
raise ValueError("notification_handlers must be a callable or a list of callables.")
|
|
139
|
+
|
|
140
|
+
# validate all items are callable
|
|
141
|
+
for handler in notification_handler:
|
|
142
|
+
if not callable(handler):
|
|
143
|
+
raise TypeError(f"Handler {handler} is not callable.")
|
|
144
|
+
global_config.register_notification(handler)
|
|
121
145
|
|
|
122
146
|
# TODO in case Python 3.12 or higher doesn't log URL
|
|
123
147
|
# if sys.version_info >= (3, 12):
|
|
@@ -129,23 +153,6 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
|
|
|
129
153
|
# return app
|
|
130
154
|
|
|
131
155
|
|
|
132
|
-
# def load_installed_plugins(app, socketio):
|
|
133
|
-
# """
|
|
134
|
-
# Dynamically load installed plugins and attach Flask-SocketIO.
|
|
135
|
-
# """
|
|
136
|
-
# plugin_names = []
|
|
137
|
-
# for entry_point in entry_points().get("ivoryos.plugins", []):
|
|
138
|
-
# plugin = entry_point.load()
|
|
139
|
-
#
|
|
140
|
-
# # If the plugin has an `init_socketio()` function, pass socketio
|
|
141
|
-
# if hasattr(plugin, 'init_socketio'):
|
|
142
|
-
# plugin.init_socketio(socketio)
|
|
143
|
-
#
|
|
144
|
-
# plugin_names.append(entry_point.name)
|
|
145
|
-
# app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
|
|
146
|
-
#
|
|
147
|
-
# return plugin_names
|
|
148
|
-
|
|
149
156
|
|
|
150
157
|
def load_plugins(blueprints: Union[list, Blueprint], app, socketio):
|
|
151
158
|
"""
|
|
@@ -103,6 +103,12 @@ function submitEditForm(event) {
|
|
|
103
103
|
document.getElementById('instrument-panel').innerHTML = previousHtmlState;
|
|
104
104
|
previousHtmlState = null; // Clear the stored state
|
|
105
105
|
}
|
|
106
|
+
const parser = new DOMParser();
|
|
107
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
108
|
+
const warningDiv = doc.querySelector('#warning');
|
|
109
|
+
if (warningDiv && warningDiv.textContent.trim()) {
|
|
110
|
+
alert(warningDiv.textContent.trim()); // or use a nicer toast
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
})
|
|
108
114
|
.catch(error => {
|
|
@@ -149,6 +155,13 @@ function duplicateAction(uuid) {
|
|
|
149
155
|
.then(response => response.text())
|
|
150
156
|
.then(html => {
|
|
151
157
|
updateActionCanvas(html);
|
|
158
|
+
|
|
159
|
+
const parser = new DOMParser();
|
|
160
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
161
|
+
const warningDiv = doc.querySelector('#warning');
|
|
162
|
+
if (warningDiv && warningDiv.textContent.trim()) {
|
|
163
|
+
alert(warningDiv.textContent.trim()); // or use a nicer toast
|
|
164
|
+
}
|
|
152
165
|
})
|
|
153
166
|
.catch(error => console.error('Error:', error));
|
|
154
167
|
}
|
|
@@ -202,6 +215,13 @@ function deleteAction(uuid) {
|
|
|
202
215
|
.then(html => {
|
|
203
216
|
// Find the first list element's content and replace it
|
|
204
217
|
updateActionCanvas(html);
|
|
218
|
+
// Optionally, check if a warning element exists
|
|
219
|
+
const parser = new DOMParser();
|
|
220
|
+
const doc = parser.parseFromString(html, 'text/html');
|
|
221
|
+
const warningDiv = doc.querySelector('#warning');
|
|
222
|
+
if (warningDiv && warningDiv.textContent.trim()) {
|
|
223
|
+
alert(warningDiv.textContent.trim()); // or use a nicer toast
|
|
224
|
+
}
|
|
205
225
|
})
|
|
206
226
|
.catch(error => console.error('Error:', error));
|
|
207
227
|
}
|
ivoryos/utils/db_models.py
CHANGED
|
@@ -434,14 +434,21 @@ class Script(db.Model):
|
|
|
434
434
|
:return: A dict containing script types as keys and lists of function body lines as values.
|
|
435
435
|
"""
|
|
436
436
|
line_collection = {}
|
|
437
|
+
|
|
437
438
|
for stype, func_str in exec_str_collection.items():
|
|
438
439
|
if func_str:
|
|
439
440
|
module = ast.parse(func_str)
|
|
440
|
-
func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
|
|
441
441
|
|
|
442
|
-
#
|
|
443
|
-
|
|
444
|
-
|
|
442
|
+
# Find the first function (regular or async)
|
|
443
|
+
func_def = next(
|
|
444
|
+
node for node in module.body
|
|
445
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Extract function body as source lines, skipping 'return' nodes
|
|
449
|
+
line_collection[stype] = [
|
|
450
|
+
ast_unparse(node) for node in func_def.body if not isinstance(node, ast.Return)
|
|
451
|
+
]
|
|
445
452
|
return line_collection
|
|
446
453
|
|
|
447
454
|
def compile(self, script_path=None):
|
|
@@ -459,7 +466,8 @@ class Script(db.Model):
|
|
|
459
466
|
|
|
460
467
|
for i in self.stypes:
|
|
461
468
|
if self.script_dict[i]:
|
|
462
|
-
|
|
469
|
+
is_async = any(a.get("coroutine", False) for a in self.script_dict[i])
|
|
470
|
+
func_str = self._generate_function_header(run_name, i, is_async) + self._generate_function_body(i)
|
|
463
471
|
exec_str_collection[i] = func_str
|
|
464
472
|
if script_path:
|
|
465
473
|
self._write_to_file(script_path, run_name, exec_str_collection)
|
|
@@ -477,7 +485,7 @@ class Script(db.Model):
|
|
|
477
485
|
name += '_'
|
|
478
486
|
return name
|
|
479
487
|
|
|
480
|
-
def _generate_function_header(self, run_name, stype):
|
|
488
|
+
def _generate_function_header(self, run_name, stype, is_async):
|
|
481
489
|
"""
|
|
482
490
|
Generate the function header.
|
|
483
491
|
"""
|
|
@@ -487,7 +495,8 @@ class Script(db.Model):
|
|
|
487
495
|
config_type.items()]
|
|
488
496
|
|
|
489
497
|
script_type = f"_{stype}" if stype != "script" else ""
|
|
490
|
-
|
|
498
|
+
async_str = "async " if is_async else ""
|
|
499
|
+
function_header = f"{async_str}def {run_name}{script_type}("
|
|
491
500
|
|
|
492
501
|
if stype == "script":
|
|
493
502
|
function_header += ", ".join(configure)
|
|
@@ -540,7 +549,8 @@ class Script(db.Model):
|
|
|
540
549
|
# elif instrument == 'registered_workflows':
|
|
541
550
|
# return inspect.getsource(my_function)
|
|
542
551
|
else:
|
|
543
|
-
|
|
552
|
+
is_async = action.get("coroutine", False)
|
|
553
|
+
return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data, is_async)
|
|
544
554
|
|
|
545
555
|
def _process_args(self, args):
|
|
546
556
|
"""
|
|
@@ -600,10 +610,12 @@ class Script(db.Model):
|
|
|
600
610
|
indent_unit -= 1
|
|
601
611
|
return exec_string, indent_unit
|
|
602
612
|
|
|
603
|
-
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data):
|
|
613
|
+
def _process_instrument_action(self, indent_unit, instrument, action, args, save_data, is_async=False):
|
|
604
614
|
"""
|
|
605
615
|
Process actions related to instruments.
|
|
606
616
|
"""
|
|
617
|
+
async_str = "await " if is_async else ""
|
|
618
|
+
|
|
607
619
|
function_call = f"{instrument}.{action}"
|
|
608
620
|
if instrument.startswith("blocks"):
|
|
609
621
|
self.blocks_included = True
|
|
@@ -611,11 +623,11 @@ class Script(db.Model):
|
|
|
611
623
|
|
|
612
624
|
if isinstance(args, dict) and args != {}:
|
|
613
625
|
args_str = self._process_dict_args(args)
|
|
614
|
-
single_line = f"{function_call}(**{args_str})"
|
|
626
|
+
single_line = f"{async_str}{function_call}(**{args_str})"
|
|
615
627
|
elif isinstance(args, str):
|
|
616
628
|
single_line = f"{function_call} = {args}"
|
|
617
629
|
else:
|
|
618
|
-
single_line = f"{function_call}()"
|
|
630
|
+
single_line = f"{async_str}{function_call}()"
|
|
619
631
|
|
|
620
632
|
if save_data:
|
|
621
633
|
save_data += " = "
|
ivoryos/utils/global_config.py
CHANGED
|
@@ -17,6 +17,8 @@ class GlobalConfig:
|
|
|
17
17
|
cls._instance._runner_lock = threading.Lock()
|
|
18
18
|
cls._instance._runner_status = None
|
|
19
19
|
cls._instance._optimizers = {}
|
|
20
|
+
cls._instance._notification_handlers = []
|
|
21
|
+
|
|
20
22
|
return cls._instance
|
|
21
23
|
|
|
22
24
|
@property
|
|
@@ -28,6 +30,15 @@ class GlobalConfig:
|
|
|
28
30
|
if self._deck is None:
|
|
29
31
|
self._deck = value
|
|
30
32
|
|
|
33
|
+
def register_notification(self, handler):
|
|
34
|
+
if not callable(handler):
|
|
35
|
+
raise ValueError("Handler must be callable")
|
|
36
|
+
self._notification_handlers.append(handler)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def notification_handlers(self):
|
|
40
|
+
return self._notification_handlers
|
|
41
|
+
|
|
31
42
|
@property
|
|
32
43
|
def building_blocks(self):
|
|
33
44
|
return self._building_blocks
|
ivoryos/utils/py_to_json.py
CHANGED
|
@@ -55,6 +55,10 @@ def convert_to_cards(source_code: str):
|
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
class CardVisitor(ast.NodeVisitor):
|
|
58
|
+
def __init__(self):
|
|
59
|
+
self.defined_types = {} # <-- always exists
|
|
60
|
+
|
|
61
|
+
|
|
58
62
|
def visit_FunctionDef(self, node):
|
|
59
63
|
self.defined_types = {
|
|
60
64
|
arg.arg: ast.unparse(arg.annotation) if arg.annotation else "float"
|
|
@@ -142,14 +146,20 @@ def convert_to_cards(source_code: str):
|
|
|
142
146
|
"return": "",
|
|
143
147
|
"uuid": generate_uuid()
|
|
144
148
|
})
|
|
149
|
+
elif isinstance(node.value, ast.Await):
|
|
150
|
+
self.handle_call(node.value.value, ret_var=node.targets[0].id, awaited=True)
|
|
151
|
+
|
|
145
152
|
elif isinstance(node.value, ast.Call):
|
|
146
153
|
self.handle_call(node.value, ret_var=node.targets[0].id)
|
|
147
154
|
|
|
148
155
|
def visit_Expr(self, node):
|
|
149
|
-
if isinstance(node.value, ast.
|
|
156
|
+
if isinstance(node.value, ast.Await):
|
|
157
|
+
# node.value is ast.Await
|
|
158
|
+
self.handle_call(node.value.value, awaited=True)
|
|
159
|
+
elif isinstance(node.value, ast.Call):
|
|
150
160
|
self.handle_call(node.value)
|
|
151
161
|
|
|
152
|
-
def handle_call(self, node, ret_var=""):
|
|
162
|
+
def handle_call(self, node, ret_var="", awaited=False):
|
|
153
163
|
func_parts = []
|
|
154
164
|
f = node.func
|
|
155
165
|
while isinstance(f, ast.Attribute):
|
|
@@ -229,7 +239,7 @@ def convert_to_cards(source_code: str):
|
|
|
229
239
|
else infer_type(value)
|
|
230
240
|
)
|
|
231
241
|
|
|
232
|
-
|
|
242
|
+
card = {
|
|
233
243
|
"action": action,
|
|
234
244
|
"arg_types": arg_types,
|
|
235
245
|
"args": args,
|
|
@@ -237,7 +247,12 @@ def convert_to_cards(source_code: str):
|
|
|
237
247
|
"instrument": instrument,
|
|
238
248
|
"return": ret_var,
|
|
239
249
|
"uuid": generate_uuid()
|
|
240
|
-
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if awaited:
|
|
253
|
+
card["coroutine"] = True # mark as coroutine if awaited
|
|
254
|
+
|
|
255
|
+
add_card(card)
|
|
241
256
|
|
|
242
257
|
CardVisitor().visit(tree)
|
|
243
258
|
return cards
|
ivoryos/utils/script_runner.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ast
|
|
2
|
+
import asyncio
|
|
2
3
|
import os
|
|
3
4
|
import csv
|
|
4
5
|
import threading
|
|
@@ -19,6 +20,14 @@ class HumanInterventionRequired(Exception):
|
|
|
19
20
|
pass
|
|
20
21
|
|
|
21
22
|
def pause(reason="Human intervention required"):
|
|
23
|
+
handlers = global_config.notification_handlers
|
|
24
|
+
if handlers:
|
|
25
|
+
for handler in handlers:
|
|
26
|
+
try:
|
|
27
|
+
handler(reason)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print(f"[notify] handler {handler} failed: {e}")
|
|
30
|
+
# raise error to pause workflow in gui
|
|
22
31
|
raise HumanInterventionRequired(reason)
|
|
23
32
|
|
|
24
33
|
class ScriptRunner:
|
|
@@ -161,7 +170,7 @@ class ScriptRunner:
|
|
|
161
170
|
start_time=datetime.now(),
|
|
162
171
|
)
|
|
163
172
|
db.session.add(step)
|
|
164
|
-
db.session.
|
|
173
|
+
db.session.flush()
|
|
165
174
|
|
|
166
175
|
logger.info(f"Executing: {line}")
|
|
167
176
|
socketio.emit('execution', {'section': f"{section_name}-{index}"})
|
|
@@ -174,7 +183,26 @@ class ScriptRunner:
|
|
|
174
183
|
duration = float(duration_str)
|
|
175
184
|
self.safe_sleep(duration)
|
|
176
185
|
else:
|
|
177
|
-
|
|
186
|
+
if "await " in line:
|
|
187
|
+
async_code = f"async def __async_exec_wrapper():\n"
|
|
188
|
+
# indent all code lines by 4 spaces
|
|
189
|
+
async_code += "\n".join(" " + line for line in line.splitlines())
|
|
190
|
+
async_code += f"\n return locals()"
|
|
191
|
+
exec(async_code, exec_globals, exec_locals)
|
|
192
|
+
func = exec_locals.get("__async_exec_wrapper") or exec_globals.get("__async_exec_wrapper")
|
|
193
|
+
# Capture the return value from asyncio.run
|
|
194
|
+
result_locals = asyncio.run(func())
|
|
195
|
+
|
|
196
|
+
# Update exec_locals with the returned locals
|
|
197
|
+
exec_locals.update(result_locals)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
else:
|
|
201
|
+
print("just exec synchronously")
|
|
202
|
+
exec(line, exec_globals, exec_locals)
|
|
203
|
+
exec_globals.update(exec_locals)
|
|
204
|
+
# return locals_dict
|
|
205
|
+
# exec(line, exec_globals, exec_locals)
|
|
178
206
|
# step.run_error = False
|
|
179
207
|
|
|
180
208
|
except HumanInterventionRequired as e:
|
|
@@ -190,6 +218,7 @@ class ScriptRunner:
|
|
|
190
218
|
|
|
191
219
|
step.run_error = True
|
|
192
220
|
self.toggle_pause()
|
|
221
|
+
exec_locals.pop("__async_exec_wrapper", None)
|
|
193
222
|
step.end_time = datetime.now()
|
|
194
223
|
step.output = exec_locals
|
|
195
224
|
db.session.commit()
|
|
@@ -219,7 +248,7 @@ class ScriptRunner:
|
|
|
219
248
|
repeat_mode=repeat_mode
|
|
220
249
|
)
|
|
221
250
|
db.session.add(run)
|
|
222
|
-
db.session.
|
|
251
|
+
db.session.flush()
|
|
223
252
|
run_id = run.id # Save the ID
|
|
224
253
|
try:
|
|
225
254
|
|
|
@@ -241,7 +270,7 @@ class ScriptRunner:
|
|
|
241
270
|
self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
|
|
242
271
|
# Reset the running flag when done
|
|
243
272
|
# Save results if necessary
|
|
244
|
-
if not script.python_script and
|
|
273
|
+
if not script.python_script and return_list:
|
|
245
274
|
filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
|
|
246
275
|
self._emit_progress(socketio, 100)
|
|
247
276
|
|
|
@@ -277,7 +306,7 @@ class ScriptRunner:
|
|
|
277
306
|
start_time=datetime.now()
|
|
278
307
|
)
|
|
279
308
|
db.session.add(phase)
|
|
280
|
-
db.session.
|
|
309
|
+
db.session.flush()
|
|
281
310
|
phase_id = phase.id
|
|
282
311
|
|
|
283
312
|
step_outputs = self.exec_steps(script, section_name, logger, socketio, phase_id=phase_id)
|
|
@@ -317,7 +346,7 @@ class ScriptRunner:
|
|
|
317
346
|
start_time=datetime.now()
|
|
318
347
|
)
|
|
319
348
|
db.session.add(phase)
|
|
320
|
-
db.session.
|
|
349
|
+
db.session.flush()
|
|
321
350
|
|
|
322
351
|
phase_id = phase.id
|
|
323
352
|
output = self.exec_steps(script, "script", logger, socketio, phase_id, **kwargs)
|
|
@@ -371,7 +400,7 @@ class ScriptRunner:
|
|
|
371
400
|
start_time=datetime.now()
|
|
372
401
|
)
|
|
373
402
|
db.session.add(phase)
|
|
374
|
-
db.session.
|
|
403
|
+
db.session.flush()
|
|
375
404
|
phase_id = phase.id
|
|
376
405
|
|
|
377
406
|
logger.info(f'Executing {run_name} experiment: {i_progress + 1}/{int(repeat_count)}')
|
ivoryos/utils/task_runner.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import asyncio
|
|
1
3
|
import threading
|
|
2
4
|
import time
|
|
3
5
|
from datetime import datetime
|
|
@@ -19,8 +21,7 @@ class TaskRunner:
|
|
|
19
21
|
self.globals_dict = globals_dict
|
|
20
22
|
self.lock = global_config.runner_lock
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
|
|
24
|
+
async def run_single_step(self, component, method, kwargs, wait=True, current_app=None):
|
|
24
25
|
global deck
|
|
25
26
|
if deck is None:
|
|
26
27
|
deck = global_config.deck
|
|
@@ -29,18 +30,18 @@ class TaskRunner:
|
|
|
29
30
|
if not self.lock.acquire(blocking=False):
|
|
30
31
|
current_status = global_config.runner_status
|
|
31
32
|
current_status["status"] = "busy"
|
|
33
|
+
current_status["output"] = "busy"
|
|
32
34
|
return current_status
|
|
33
35
|
|
|
34
|
-
|
|
35
36
|
if wait:
|
|
36
|
-
output = self._run_single_step(component, method, kwargs, current_app)
|
|
37
|
+
output = await self._run_single_step(component, method, kwargs, current_app)
|
|
37
38
|
else:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
# Create background task properly
|
|
40
|
+
async def background_runner():
|
|
41
|
+
await self._run_single_step(component, method, kwargs, current_app)
|
|
42
|
+
|
|
43
|
+
asyncio.create_task(background_runner())
|
|
44
|
+
await asyncio.sleep(0.1) # Change time.sleep to await asyncio.sleep
|
|
44
45
|
output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
|
|
45
46
|
|
|
46
47
|
return output
|
|
@@ -59,22 +60,32 @@ class TaskRunner:
|
|
|
59
60
|
function_executable = getattr(instrument, method)
|
|
60
61
|
return function_executable
|
|
61
62
|
|
|
62
|
-
def _run_single_step(self, component, method, kwargs, current_app=None):
|
|
63
|
+
async def _run_single_step(self, component, method, kwargs, current_app=None):
|
|
63
64
|
try:
|
|
64
65
|
function_executable = self._get_executable(component, deck, method)
|
|
65
66
|
method_name = f"{component}.{method}"
|
|
66
67
|
except Exception as e:
|
|
67
68
|
self.lock.release()
|
|
68
|
-
return {"status": "error", "msg": e
|
|
69
|
+
return {"status": "error", "msg": str(e)}
|
|
69
70
|
|
|
70
|
-
# with
|
|
71
|
+
# Flask context is NOT async → just use normal "with"
|
|
71
72
|
with current_app.app_context():
|
|
72
|
-
step = SingleStep(
|
|
73
|
+
step = SingleStep(
|
|
74
|
+
method_name=method_name,
|
|
75
|
+
kwargs=kwargs,
|
|
76
|
+
run_error=None,
|
|
77
|
+
start_time=datetime.now()
|
|
78
|
+
)
|
|
73
79
|
db.session.add(step)
|
|
74
|
-
db.session.
|
|
75
|
-
global_config.runner_status = {"id":step.id, "type": "task"}
|
|
80
|
+
db.session.flush()
|
|
81
|
+
global_config.runner_status = {"id": step.id, "type": "task"}
|
|
82
|
+
|
|
76
83
|
try:
|
|
77
|
-
|
|
84
|
+
if inspect.iscoroutinefunction(function_executable):
|
|
85
|
+
output = await function_executable(**kwargs)
|
|
86
|
+
else:
|
|
87
|
+
output = function_executable(**kwargs)
|
|
88
|
+
|
|
78
89
|
step.output = output
|
|
79
90
|
step.end_time = datetime.now()
|
|
80
91
|
success = True
|
|
@@ -86,4 +97,5 @@ class TaskRunner:
|
|
|
86
97
|
finally:
|
|
87
98
|
db.session.commit()
|
|
88
99
|
self.lock.release()
|
|
89
|
-
|
|
100
|
+
|
|
101
|
+
return dict(success=success, output=output)
|
ivoryos/utils/utils.py
CHANGED
|
@@ -105,7 +105,8 @@ def _inspect_class(class_object=None, debug=False):
|
|
|
105
105
|
try:
|
|
106
106
|
annotation = inspect.signature(method)
|
|
107
107
|
docstring = inspect.getdoc(method)
|
|
108
|
-
|
|
108
|
+
coroutine = inspect.iscoroutinefunction(method)
|
|
109
|
+
functions[function] = dict(signature=annotation, docstring=docstring, coroutine=coroutine,)
|
|
109
110
|
|
|
110
111
|
except Exception:
|
|
111
112
|
pass
|
|
@@ -141,6 +142,7 @@ def _get_type_from_parameters(arg, parameters):
|
|
|
141
142
|
def _convert_by_str(args, arg_types):
|
|
142
143
|
"""
|
|
143
144
|
Converts a value to type through eval(f'{type}("{args}")')
|
|
145
|
+
v1.3.4 TODO try str lastly, otherwise it's always converted to str
|
|
144
146
|
"""
|
|
145
147
|
if type(arg_types) is not list:
|
|
146
148
|
arg_types = [arg_types]
|
ivoryos/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.3.
|
|
1
|
+
__version__ = "1.3.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ivoryos
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.5
|
|
4
4
|
Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
|
|
5
5
|
Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
|
|
6
6
|
License: MIT
|
|
@@ -17,9 +17,19 @@ Requires-Dist: Flask-WTF
|
|
|
17
17
|
Requires-Dist: SQLAlchemy-Utils
|
|
18
18
|
Requires-Dist: python-dotenv
|
|
19
19
|
Requires-Dist: astor; python_version < "3.9"
|
|
20
|
+
Provides-Extra: optimizer-ax
|
|
21
|
+
Requires-Dist: ax-platform; extra == "optimizer-ax"
|
|
22
|
+
Provides-Extra: optimizer-baybe
|
|
23
|
+
Requires-Dist: baybe; extra == "optimizer-baybe"
|
|
20
24
|
Provides-Extra: optimizer
|
|
21
25
|
Requires-Dist: ax-platform; extra == "optimizer"
|
|
22
26
|
Requires-Dist: baybe; extra == "optimizer"
|
|
27
|
+
Provides-Extra: doc
|
|
28
|
+
Requires-Dist: sphinx; extra == "doc"
|
|
29
|
+
Requires-Dist: sphinx-rtd-theme; extra == "doc"
|
|
30
|
+
Requires-Dist: sphinxcontrib-httpdomain; extra == "doc"
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest; extra == "dev"
|
|
23
33
|
Dynamic: license-file
|
|
24
34
|
|
|
25
35
|
[](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
|
|
@@ -28,7 +38,8 @@ Dynamic: license-file
|
|
|
28
38
|
[](https://youtu.be/dFfJv9I2-1g)
|
|
29
39
|
[](https://youtu.be/flr5ydiE96s)
|
|
30
40
|
[](https://www.nature.com/articles/s41467-025-60514-w)
|
|
31
|
-
|
|
41
|
+
|
|
42
|
+
[//]: # ([](https://discord.gg/AX5P9EdGVX))
|
|
32
43
|
|
|
33
44
|

|
|
34
45
|
# ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
|
|
@@ -101,11 +112,14 @@ pip install -e .
|
|
|
101
112
|
## Quick start
|
|
102
113
|
In your SDL script,
|
|
103
114
|
```python
|
|
115
|
+
my_robot = Robot()
|
|
116
|
+
|
|
104
117
|
import ivoryos
|
|
105
118
|
|
|
106
119
|
ivoryos.run(__name__)
|
|
107
120
|
```
|
|
108
|
-
|
|
121
|
+
You can now access the web UI at http://127.0.0.1:8000,
|
|
122
|
+
create an account, login, and start designing workflows!
|
|
109
123
|
|
|
110
124
|
----
|
|
111
125
|
## Features
|
|
@@ -127,6 +141,28 @@ Add single or multiple loggers:
|
|
|
127
141
|
ivoryos.run(__name__, logger="logger name")
|
|
128
142
|
ivoryos.run(__name__, logger=["logger 1", "logger 2"])
|
|
129
143
|
```
|
|
144
|
+
### Human-in-the-loop
|
|
145
|
+
Add single or multiple notification handlers for `pause` feature in flow control:
|
|
146
|
+
```python
|
|
147
|
+
|
|
148
|
+
def slack_bot(msg: str = "Hi"):
|
|
149
|
+
"""
|
|
150
|
+
a function that can be used as a notification handler function("msg")
|
|
151
|
+
:param msg: message to send
|
|
152
|
+
"""
|
|
153
|
+
from slack_sdk import WebClient
|
|
154
|
+
|
|
155
|
+
slack_token = "your slack token"
|
|
156
|
+
client = WebClient(token=slack_token)
|
|
157
|
+
|
|
158
|
+
my_user_id = "your user id" # replace with your actual Slack user ID
|
|
159
|
+
|
|
160
|
+
client.chat_postMessage(channel=my_user_id, text=msg)
|
|
161
|
+
|
|
162
|
+
import ivoryos
|
|
163
|
+
ivoryos.run(__name__, notification_handler=slack_bot)
|
|
164
|
+
```
|
|
165
|
+
|
|
130
166
|
### Directory Structure
|
|
131
167
|
|
|
132
168
|
Created automatically on first run:
|
|
@@ -152,11 +188,10 @@ ivoryos.run(__name__)
|
|
|
152
188
|
|
|
153
189
|
## Roadmap
|
|
154
190
|
|
|
155
|
-
- [x] Allow plugin pages ✅
|
|
156
|
-
- [x] pause, resume, abort current and pending workflows ✅
|
|
157
191
|
- [ ] dropdown input
|
|
158
192
|
- [ ] snapshot version control
|
|
159
193
|
- [ ] optimizer-agnostic
|
|
194
|
+
- [ ] prefect compatibility
|
|
160
195
|
- [ ] check batch-config file compatibility
|
|
161
196
|
|
|
162
197
|
---
|
|
@@ -198,4 +233,4 @@ For an additional perspective related to the development of the tool, please see
|
|
|
198
233
|
```
|
|
199
234
|
---
|
|
200
235
|
## Acknowledgements
|
|
201
|
-
Authors acknowledge Telescope Innovations Corp., Hein Lab members for their valuable suggestions and contributions.
|
|
236
|
+
Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
ivoryos/__init__.py,sha256=
|
|
2
|
-
ivoryos/app.py,sha256=
|
|
1
|
+
ivoryos/__init__.py,sha256=gEvBO2y5TRq06Itjjej3iAcq73UsihqKPWcb2HykPwM,463
|
|
2
|
+
ivoryos/app.py,sha256=G6kzEOVzCduj7Fc2r1rMbMFHgDzZQV0lC20Oxps7RSM,4839
|
|
3
3
|
ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
|
|
4
|
-
ivoryos/server.py,sha256=
|
|
4
|
+
ivoryos/server.py,sha256=5tXrgG16lm6Fl-Q-ZHxiGQQoyZQFxRhdDf6idNYqhNg,6985
|
|
5
5
|
ivoryos/socket_handlers.py,sha256=VWVWiIdm4jYAutwGu6R0t1nK5MuMyOCL0xAnFn06jWQ,1302
|
|
6
|
-
ivoryos/version.py,sha256=
|
|
6
|
+
ivoryos/version.py,sha256=tdqvkGH0OryRjjXzO3HS5DyYol-VTO9fC8m43nB2PgI,22
|
|
7
7
|
ivoryos/optimizer/ax_optimizer.py,sha256=PoSu8hrDFFpqyhRBnaSMswIUsDfEX6sPWt8NEZ_sobs,7112
|
|
8
8
|
ivoryos/optimizer/base_optimizer.py,sha256=JTbUharZKn0t8_BDbAFuwZIbT1VOnX1Xuog1pJuU8hY,1992
|
|
9
9
|
ivoryos/optimizer/baybe_optimizer.py,sha256=EdrrRiYO-IOx610cPXiQhH4qG8knUP0uiZ0YoyaGIU8,7954
|
|
@@ -15,7 +15,7 @@ ivoryos/routes/auth/auth.py,sha256=D-sPEaUsc8vefly36h74rgU-ILulf7hiK6xc5sBEl58,3
|
|
|
15
15
|
ivoryos/routes/auth/templates/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
|
|
16
16
|
ivoryos/routes/auth/templates/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
|
|
17
17
|
ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
ivoryos/routes/control/control.py,sha256=
|
|
18
|
+
ivoryos/routes/control/control.py,sha256=0g23PoPPoTeMD7Szm3UAW10sEMLpSD-iTfASAZE4iIE,6442
|
|
19
19
|
ivoryos/routes/control/control_file.py,sha256=3fQ9R8EcdqKs_hABn2EqRAB1xC2DHAT_q_pwsMIDDQI,864
|
|
20
20
|
ivoryos/routes/control/control_new_device.py,sha256=mfJKg5JAOagIpUKbp2b5nRwvd2V3bzT3M0zIhIsEaFM,5456
|
|
21
21
|
ivoryos/routes/control/utils.py,sha256=XlhhqAtOj7n3XfHPDxJ8TvCV2K2I2IixB0CBkl1QeQc,1242
|
|
@@ -27,9 +27,9 @@ ivoryos/routes/data/templates/workflow_database.html,sha256=ofvHcovpwmJXo1SFiSrL
|
|
|
27
27
|
ivoryos/routes/data/templates/workflow_view.html,sha256=Ti17kzlPlYTmzx5MkdsPlXJ1_k6QgMYQBM6FHjG50go,12491
|
|
28
28
|
ivoryos/routes/data/templates/components/step_card.html,sha256=XWsr7qxAY76RCuQHETubWjWBlPgs2HkviH4ju6qfBKo,1923
|
|
29
29
|
ivoryos/routes/design/__init__.py,sha256=zS3HXKaw0ALL5n6t_W1rUz5Uj5_tTQ-Y1VMXyzewvR0,113
|
|
30
|
-
ivoryos/routes/design/design.py,sha256=
|
|
30
|
+
ivoryos/routes/design/design.py,sha256=8xWsJxs-jg3r1yszzvHWK0mccYoYlgzhO_dTV7aEA_w,18567
|
|
31
31
|
ivoryos/routes/design/design_file.py,sha256=MVIc5uGSaGxZhs86hfPjX2n0iy1OcXeLq7b9Ucdg4VQ,2115
|
|
32
|
-
ivoryos/routes/design/design_step.py,sha256=
|
|
32
|
+
ivoryos/routes/design/design_step.py,sha256=maDaR-CFK6UY_ajbQeYJ_onwYW0e9nMD-HgrhsSU6JY,5874
|
|
33
33
|
ivoryos/routes/design/templates/experiment_builder.html,sha256=hh-d2tOc_40gww5WfUYIf8sM3qBaALZnR8Sx7Ja4tpU,1623
|
|
34
34
|
ivoryos/routes/design/templates/components/action_form.html,sha256=kXJOrJLbFsMHHWVSuMQHpt1xFrUMnwgzTG8e6Qfn0Cg,3042
|
|
35
35
|
ivoryos/routes/design/templates/components/actions_panel.html,sha256=jHTR58saTUIZInBdC-vLc1ZTbStLiULeWbupjB4hQzo,977
|
|
@@ -37,7 +37,7 @@ ivoryos/routes/design/templates/components/autofill_toggle.html,sha256=CRVQUHoQT
|
|
|
37
37
|
ivoryos/routes/design/templates/components/canvas.html,sha256=bKLCJaG1B36Yy9Vsnz4P5qiX4BPdfaGe9JeQQzu9rsI,268
|
|
38
38
|
ivoryos/routes/design/templates/components/canvas_footer.html,sha256=5VRRacMZbzx0hUej0NPP-PmXM_AtUqduHzDS7a60cQY,435
|
|
39
39
|
ivoryos/routes/design/templates/components/canvas_header.html,sha256=7iIzLDGHX7MnmBbf98nWtLDprbeIgoNV4dJUO1zE4Tc,3598
|
|
40
|
-
ivoryos/routes/design/templates/components/canvas_main.html,sha256=
|
|
40
|
+
ivoryos/routes/design/templates/components/canvas_main.html,sha256=nLEtp3U2YtfJwob1kR8ua8-UVdu9hwc6z1L5UMNVz8c,1524
|
|
41
41
|
ivoryos/routes/design/templates/components/deck_selector.html,sha256=ryTRpljYezo0AzGLCJu_qOMokjjnft3GIxddmNGtBA0,657
|
|
42
42
|
ivoryos/routes/design/templates/components/edit_action_form.html,sha256=Dz7FnnOK4PYptAHNy9_WFCU1RZTSV61-1lNHHOSRJNs,1876
|
|
43
43
|
ivoryos/routes/design/templates/components/instruments_panel.html,sha256=tRKd-wOqKjaMJCLuGgRmHtxIgSjklhBkuX8arm5aTCU,4268
|
|
@@ -74,7 +74,7 @@ ivoryos/static/logo.webp,sha256=lXgfQR-4mHTH83k7VV9iB54-oC2ipe6uZvbwdOnLETc,1497
|
|
|
74
74
|
ivoryos/static/style.css,sha256=zQVx35A5g6JMJ-K84-6fSKtzXGjp_p5ZVG6KLHPM2IE,4021
|
|
75
75
|
ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
|
|
76
76
|
ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
|
|
77
|
-
ivoryos/static/js/action_handlers.js,sha256=
|
|
77
|
+
ivoryos/static/js/action_handlers.js,sha256=DnNYV5mITADKp14I6NaByNRzHco0dDPP2fuSM43x2hY,6119
|
|
78
78
|
ivoryos/static/js/db_delete.js,sha256=l67fqUaN_FVDaL7v91Hd7LyRbxnqXx9nyjF34-7aewY,561
|
|
79
79
|
ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
|
|
80
80
|
ivoryos/static/js/script_metadata.js,sha256=m8VYZ8OGT2oTx1kXMXq60bKQI9WCbJNkzcFDzLvRuGc,1188
|
|
@@ -86,18 +86,18 @@ ivoryos/templates/base.html,sha256=cl5w6E8yskbUzdiJFal6fZjnPuFNKEzc7BrrbRd6bMI,8
|
|
|
86
86
|
ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
87
|
ivoryos/utils/bo_campaign.py,sha256=Fil-zT7JexL_p9XqyWByjAk42XB1R9XUKN8CdV5bi6c,9714
|
|
88
88
|
ivoryos/utils/client_proxy.py,sha256=74G3HAuq50iEHkSvlMZFmQaukm613FbRgOdzO_T3dMg,10191
|
|
89
|
-
ivoryos/utils/db_models.py,sha256=
|
|
89
|
+
ivoryos/utils/db_models.py,sha256=i1fLiWFnese2xqAfWipN9eUDHeuy6eIYE9_cY6YZ6NU,31553
|
|
90
90
|
ivoryos/utils/decorators.py,sha256=p1Bdl3dCeaHNv6-cCCUOZMiFu9kRaqqQnkFJUkzPoJE,991
|
|
91
91
|
ivoryos/utils/form.py,sha256=Ej9tx06KZZ5fPQm1ho1byotNocF3u24aatc2ZyI0rK4,22301
|
|
92
|
-
ivoryos/utils/global_config.py,sha256=
|
|
92
|
+
ivoryos/utils/global_config.py,sha256=leYoEXvAS0AH4xQpYsqu4HI9CJ9-wiLM-pIh_bEG4Ak,3087
|
|
93
93
|
ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
|
|
94
|
-
ivoryos/utils/py_to_json.py,sha256=
|
|
95
|
-
ivoryos/utils/script_runner.py,sha256=
|
|
94
|
+
ivoryos/utils/py_to_json.py,sha256=ZtejHgwdEAUCVVMYeVNR8G7ceLINue294q6WpiJ6jn0,9734
|
|
95
|
+
ivoryos/utils/script_runner.py,sha256=qozSrlpnsk5rZzui8WT59cMaw41ZfsYKvofAXdDiC5A,20747
|
|
96
96
|
ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
|
|
97
|
-
ivoryos/utils/task_runner.py,sha256=
|
|
98
|
-
ivoryos/utils/utils.py,sha256=
|
|
99
|
-
ivoryos-1.3.
|
|
100
|
-
ivoryos-1.3.
|
|
101
|
-
ivoryos-1.3.
|
|
102
|
-
ivoryos-1.3.
|
|
103
|
-
ivoryos-1.3.
|
|
97
|
+
ivoryos/utils/task_runner.py,sha256=xiMzK8gQ0mHsg0A1Ah8fmXe3azpaJh4hJiQJLHA11ZQ,3682
|
|
98
|
+
ivoryos/utils/utils.py,sha256=dIc4BO55eS3lCA0nhbIncS5d7sLaKZy5hJS1I_Sm45o,14949
|
|
99
|
+
ivoryos-1.3.5.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
|
|
100
|
+
ivoryos-1.3.5.dist-info/METADATA,sha256=z3rNHhq2GaCBN6bWwi6JtUXn8fWzkigMR-_PuKHgFtc,8403
|
|
101
|
+
ivoryos-1.3.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
102
|
+
ivoryos-1.3.5.dist-info/top_level.txt,sha256=FRIWWdiEvRKqw-XfF_UK3XV0CrnNb6EmVbEgjaVazRM,8
|
|
103
|
+
ivoryos-1.3.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|