ivoryos 0.1.18__tar.gz → 0.1.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (54) hide show
  1. {ivoryos-0.1.18/ivoryos.egg-info → ivoryos-0.1.20}/PKG-INFO +12 -4
  2. {ivoryos-0.1.18 → ivoryos-0.1.20}/README.md +11 -3
  3. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/design/design.py +24 -8
  4. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/design/templates/design/experiment_run.html +49 -6
  5. ivoryos-0.1.20/ivoryos/static/js/socket_handler.js +72 -0
  6. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/db_models.py +27 -42
  7. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/script_runner.py +91 -40
  8. ivoryos-0.1.20/ivoryos/version.py +1 -0
  9. {ivoryos-0.1.18 → ivoryos-0.1.20/ivoryos.egg-info}/PKG-INFO +12 -4
  10. ivoryos-0.1.18/ivoryos/static/js/socket_handler.js +0 -34
  11. ivoryos-0.1.18/ivoryos/version.py +0 -1
  12. {ivoryos-0.1.18 → ivoryos-0.1.20}/LICENSE +0 -0
  13. {ivoryos-0.1.18 → ivoryos-0.1.20}/MANIFEST.in +0 -0
  14. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/__init__.py +0 -0
  15. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/config.py +0 -0
  16. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/__init__.py +0 -0
  17. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/auth/__init__.py +0 -0
  18. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/auth/auth.py +0 -0
  19. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/auth/templates/auth/login.html +0 -0
  20. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/auth/templates/auth/signup.html +0 -0
  21. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/control/__init__.py +0 -0
  22. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/control/control.py +0 -0
  23. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/control/templates/control/controllers.html +0 -0
  24. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/control/templates/control/controllers_home.html +0 -0
  25. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/control/templates/control/controllers_new.html +0 -0
  26. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/database/__init__.py +0 -0
  27. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/database/database.py +0 -0
  28. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/database/templates/database/experiment_database.html +0 -0
  29. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/design/__init__.py +0 -0
  30. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/design/templates/design/experiment_builder.html +0 -0
  31. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/main/__init__.py +0 -0
  32. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/main/main.py +0 -0
  33. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/main/templates/main/help.html +0 -0
  34. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/routes/main/templates/main/home.html +0 -0
  35. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/favicon.ico +0 -0
  36. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  37. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  38. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/js/overlay.js +0 -0
  39. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/js/sortable_card.js +0 -0
  40. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/js/sortable_design.js +0 -0
  41. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/logo.webp +0 -0
  42. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/static/style.css +0 -0
  43. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/templates/base.html +0 -0
  44. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/__init__.py +0 -0
  45. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/form.py +0 -0
  46. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/global_config.py +0 -0
  47. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/llm_agent.py +0 -0
  48. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos/utils/utils.py +0 -0
  49. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos.egg-info/SOURCES.txt +0 -0
  50. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos.egg-info/dependency_links.txt +0 -0
  51. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos.egg-info/requires.txt +0 -0
  52. {ivoryos-0.1.18 → ivoryos-0.1.20}/ivoryos.egg-info/top_level.txt +0 -0
  53. {ivoryos-0.1.18 → ivoryos-0.1.20}/setup.cfg +0 -0
  54. {ivoryos-0.1.18 → ivoryos-0.1.20}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivoryos
3
- Version: 0.1.18
3
+ Version: 0.1.20
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
@@ -13,6 +13,9 @@ License-File: LICENSE
13
13
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
14
14
  [![PyPI version](https://img.shields.io/pypi/v/ivoryos)](https://pypi.org/project/ivoryos/)
15
15
  ![License](https://img.shields.io/pypi/l/ivoryos)
16
+ [![YouTube](https://img.shields.io/badge/YouTube-video-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
17
+ [![Research Square](https://img.shields.io/badge/Preprint-blue)](https://www.researchsquare.com/article/rs-5307798/v1)
18
+
16
19
 
17
20
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
18
21
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -24,6 +27,7 @@ License-File: LICENSE
24
27
  - [Installation](#installation)
25
28
  - [Instructions for use](#instructions-for-use)
26
29
  - [Demo](#demo)
30
+ - [Roadmap](#roadmap)
27
31
  - [License](#license)
28
32
 
29
33
  ## Description
@@ -152,9 +156,13 @@ When you run the application for the first time, it will automatically create th
152
156
  - **`ivoryos.db`**: Database file that stores application data locally.
153
157
 
154
158
 
155
- ### Demo video
156
- Intro + Tutorial + Demo with PurPOSE platform
157
- https://youtu.be/dFfJv9I2-1g
159
+ ## Roadmap
160
+
161
+ - [x] Allow plugin pages ✅
162
+ - [x] pause, resume, abort current and pending workflows ✅
163
+ - [ ] snapshot version control
164
+ - [ ] dropdown input
165
+ - [ ] show line number option
158
166
 
159
167
 
160
168
  ## Authors and Acknowledgement
@@ -1,6 +1,9 @@
1
1
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
2
2
  [![PyPI version](https://img.shields.io/pypi/v/ivoryos)](https://pypi.org/project/ivoryos/)
3
3
  ![License](https://img.shields.io/pypi/l/ivoryos)
4
+ [![YouTube](https://img.shields.io/badge/YouTube-video-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
5
+ [![Research Square](https://img.shields.io/badge/Preprint-blue)](https://www.researchsquare.com/article/rs-5307798/v1)
6
+
4
7
 
5
8
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
6
9
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -12,6 +15,7 @@
12
15
  - [Installation](#installation)
13
16
  - [Instructions for use](#instructions-for-use)
14
17
  - [Demo](#demo)
18
+ - [Roadmap](#roadmap)
15
19
  - [License](#license)
16
20
 
17
21
  ## Description
@@ -140,9 +144,13 @@ When you run the application for the first time, it will automatically create th
140
144
  - **`ivoryos.db`**: Database file that stores application data locally.
141
145
 
142
146
 
143
- ### Demo video
144
- Intro + Tutorial + Demo with PurPOSE platform
145
- https://youtu.be/dFfJv9I2-1g
147
+ ## Roadmap
148
+
149
+ - [x] Allow plugin pages ✅
150
+ - [x] pause, resume, abort current and pending workflows ✅
151
+ - [ ] snapshot version control
152
+ - [ ] dropdown input
153
+ - [ ] show line number option
146
154
 
147
155
 
148
156
  ## Authors and Acknowledgement
@@ -24,10 +24,22 @@ global_config = GlobalConfig()
24
24
  runner = ScriptRunner()
25
25
 
26
26
 
27
- @socketio.on('abort_action')
28
- def handle_abort_action():
27
+ @socketio.on('abort_pending')
28
+ def handle_abort_pending():
29
+ runner.abort_pending()
30
+ socketio.emit('log', {'message': "aborted pending iterations"})
31
+
32
+
33
+ @socketio.on('abort_current')
34
+ def handle_abort_current():
29
35
  runner.stop_execution()
30
- socketio.emit('log', {'message': "aborted pending tasks"})
36
+ socketio.emit('log', {'message': "stopped next task"})
37
+
38
+
39
+ @socketio.on('pause')
40
+ def handle_pause():
41
+ msg = runner.toggle_pause()
42
+ socketio.emit('log', {'message': msg})
31
43
 
32
44
 
33
45
  @socketio.on('connect')
@@ -266,9 +278,11 @@ def experiment_run():
266
278
  config_preview = config[1:6]
267
279
  arg_type = config.pop(0) # first entry is types
268
280
  try:
269
- exec(exec_string)
281
+ for key, func_str in exec_string.items():
282
+ exec(func_str)
283
+ line_collection = script.convert_to_lines(exec_string)
270
284
  except Exception:
271
- flash("Please check syntax!!")
285
+ flash(f"Please check {key} syntax!!")
272
286
  return redirect(url_for("design.experiment_builder"))
273
287
  # runner.globals_dict.update(globals())
274
288
  run_name = script.name if script.name else "untitled"
@@ -310,10 +324,12 @@ def experiment_run():
310
324
  flash(f"WARNING: Duplicate in config entries.")
311
325
  except Exception as e:
312
326
  flash(e)
313
- return render_template('experiment_run.html', script=script.script_dict, filename=filename, dot_py=exec_string,
327
+ return render_template('experiment_run.html', script=script.script_dict, filename=filename,
328
+ dot_py=exec_string, line_collection=line_collection,
314
329
  return_list=return_list, config_list=config_list, config_file_list=config_file_list,
315
330
  config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
316
- no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons, history=deck_list)
331
+ no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
332
+ history=deck_list)
317
333
 
318
334
 
319
335
  @design.route("/toggle_script_type/<stype>")
@@ -547,4 +563,4 @@ def duplicate_action(id: int):
547
563
  script = utils.get_script_file()
548
564
  script.duplicate_action(id)
549
565
  utils.post_script_file(script)
550
- return redirect(back)
566
+ return redirect(back)
@@ -28,7 +28,7 @@
28
28
  <div class="accordion-body">
29
29
  {% if script['script'] or script['prep'] or script['cleanup'] %}
30
30
  <div class="row">
31
- <div class="col-lg-6 col-sm-12">
31
+ <div class="col-lg-6 col-sm-12" id="run-panel">
32
32
  <ul class="nav nav-tabs" id="myTabs" role="tablist">
33
33
  <li class="nav-item" role="presentation">
34
34
  <a class="nav-link {{ 'disabled' if config_list else '' }} {{ 'active' if not config_list else '' }}" id="tab1-tab" data-bs-toggle="tab" href="#tab1" role="tab" aria-controls="tab1" aria-selected="false">Repeat</a>
@@ -217,13 +217,36 @@
217
217
  </div>
218
218
  </div>
219
219
  </div>
220
- <div class="col-lg-6 col-sm-12 logging-panel">
220
+ <div class="col-lg-6 col-sm-12" id="code-panel" style="display: none;">
221
221
  <p>
222
- <div class="p d-flex justify-content-between align-items-center">
223
222
  <h5>Progress:</h5>
224
- <button id="abort" class="btn btn-danger ">Abort Pending Actions</button>
225
- </div>
223
+ {% for stype, script in line_collection.items() %}
224
+ {% for code in script %}
225
+ <pre style="margin: 0; padding: 0; line-height: 1;"><code class="python" id="{{ stype }}-{{ loop.index0 }}" >{{code}}</code></pre>
226
+ {% endfor %}
227
+ {% endfor %}
226
228
  </p>
229
+ </div>
230
+ <div class="col-lg-6 col-sm-12 logging-panel">
231
+ <p>
232
+ <div class="p d-flex justify-content-between align-items-center">
233
+ <h5>Progress:</h5>
234
+ <div class="d-flex gap-2 ms-auto">
235
+ <button id="pause-resume" class="btn btn-info text-white" data-bs-toggle="tooltip" title="Pause execution">
236
+ <i class="bi bi-pause-circle"></i> <!-- Icon for Pause -->
237
+ </button>
238
+ <button id="abort-current" class="btn btn-danger text-white" data-bs-toggle="tooltip" title="Stop execution after current step">
239
+ <i class="bi bi-stop-circle"></i> <!-- Icon for Stop After This Step -->
240
+ </button>
241
+ <button id="abort-pending" class="btn btn-warning text-white" data-bs-toggle="tooltip" title="Stop execution after current iteration">
242
+ <i class="bi bi-hourglass-split"></i> <!-- Icon for Stop After This Iteration -->
243
+ </button>
244
+ </div>
245
+ </div>
246
+ <div class="text-muted mt-2">
247
+ <small><strong>Note:</strong> The current step cannot be paused or stopped until it completes. </small>
248
+ </div>
249
+
227
250
  <div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="10" aria-valuemin="0" aria-valuemax="100">
228
251
  <div id="progress-bar-inner" class="progress-bar progress-bar-striped progress-bar-animated"></div>
229
252
  </div>
@@ -295,7 +318,9 @@
295
318
  </h2>
296
319
  <div id="python" class="accordion-collapse collapse">
297
320
  <div class="accordion-body">
298
- <pre><code class="python" >{{dot_py}}</code></pre>
321
+ {% for stype, script in dot_py.items() %}
322
+ <pre><code class="python" >{{script}}</code></pre>
323
+ {% endfor %}
299
324
  <a href="{{ url_for('design.download', filetype='python') }}">Download <i class="bi bi-download"></i></a>
300
325
  </div>
301
326
  </div>
@@ -322,4 +347,22 @@
322
347
  }
323
348
  });
324
349
  </script>
350
+
351
+ <script>
352
+ var socket = io.connect(window.location.origin);
353
+
354
+ socket.on('execution', function(data) {
355
+ // Remove highlighting from all lines
356
+ document.querySelectorAll('pre code').forEach(el => el.style.backgroundColor = '');
357
+
358
+ // Get the currently executing line and highlight it
359
+ let executingLine = document.getElementById(data.section);
360
+ if (executingLine) {
361
+ executingLine.style.backgroundColor = '#cce5ff'; // Highlight
362
+ executingLine.style.transition = 'background-color 0.3s ease-in-out';
363
+
364
+ }
365
+ });
366
+ </script>
367
+
325
368
  {% endblock %}
@@ -0,0 +1,72 @@
1
+ document.addEventListener("DOMContentLoaded", function() {
2
+ var socket = io.connect('http://' + document.domain + ':' + location.port);
3
+ socket.on('connect', function() {
4
+ console.log('Connected');
5
+ });
6
+ socket.on('progress', function(data) {
7
+ var progress = data.progress;
8
+ console.log(progress);
9
+ // Update the progress bar's width and appearance
10
+ var progressBar = document.getElementById('progress-bar-inner');
11
+ progressBar.style.width = progress + '%';
12
+ progressBar.setAttribute('aria-valuenow', progress);
13
+ if (progress === 1) {
14
+ document.getElementById("run-panel").style.display = "none";
15
+ document.getElementById("code-panel").style.display = "block";
16
+
17
+ // Optional: Scroll to the code panel
18
+ document.getElementById("code-panel").scrollIntoView({ behavior: "smooth" });
19
+ }
20
+ if (progress === 100) {
21
+ // Remove animation and set green color when 100% is reached
22
+ progressBar.classList.remove('progress-bar-animated');
23
+ progressBar.classList.add('bg-success'); // Bootstrap class for green color
24
+ setTimeout(() => {
25
+ document.getElementById("code-panel").style.display = "none";
26
+ document.getElementById("run-panel").style.display = "block";
27
+ }, 1000); // Small delay to let users see the completion
28
+ }
29
+ });
30
+ socket.on('log', function(data) {
31
+ var logMessage = data.message;
32
+ console.log(logMessage);
33
+ $('#logging-panel').append(logMessage + "<br>");
34
+ $('#logging-panel').scrollTop($('#logging-panel')[0].scrollHeight);
35
+ });
36
+
37
+ document.getElementById('abort-pending').addEventListener('click', function() {
38
+ var confirmation = confirm("Are you sure you want to stop after this iteration?");
39
+ if (confirmation) {
40
+ socket.emit('abort_pending');
41
+ console.log('Abort action sent to server.');
42
+ }
43
+ });
44
+ document.getElementById('abort-current').addEventListener('click', function() {
45
+ var confirmation = confirm("Are you sure you want to stop after this step?");
46
+ if (confirmation) {
47
+ socket.emit('abort_current');
48
+ console.log('Stop action sent to server.');
49
+ }
50
+ });
51
+
52
+ document.getElementById('pause-resume').addEventListener('click', function() {
53
+
54
+ socket.emit('pause');
55
+ console.log('Pause/Resume is toggled.');
56
+ var button = this;
57
+ var icon = button.querySelector("i");
58
+
59
+ // Toggle between Pause and Resume
60
+ if (icon.classList.contains("bi-pause-circle")) {
61
+ icon.classList.remove("bi-pause-circle");
62
+ icon.classList.add("bi-play-circle");
63
+ button.innerHTML = '<i class="bi bi-play-circle"></i>';
64
+ button.setAttribute("title", "Resume execution");
65
+ } else {
66
+ icon.classList.remove("bi-play-circle");
67
+ icon.classList.add("bi-pause-circle");
68
+ button.innerHTML = '<i class="bi bi-pause-circle"></i>';
69
+ button.setAttribute("title", "Pause execution");
70
+ }
71
+ });
72
+ });
@@ -390,6 +390,23 @@ class Script(db.Model):
390
390
  string += "\t"
391
391
  return string
392
392
 
393
+ def convert_to_lines(self, exec_str_collection: dict):
394
+ """
395
+ Parse a dictionary of script functions and extract function body lines.
396
+
397
+ :param exec_str_collection: Dictionary containing script types and corresponding function strings.
398
+ :return: A dict containing script types as keys and lists of function body lines as values.
399
+ """
400
+ line_collection = {}
401
+ for stype, func_str in exec_str_collection.items():
402
+ module = ast.parse(func_str)
403
+ func_def = next(node for node in module.body if isinstance(node, ast.FunctionDef))
404
+
405
+ # Extract function body as source lines
406
+ line_collection[stype] = [ast.unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
407
+ # print(line_collection[stype])
408
+ return line_collection
409
+
393
410
  def compile(self, script_path=None):
394
411
  """
395
412
  Compile the current script to a Python file.
@@ -398,35 +415,17 @@ class Script(db.Model):
398
415
  self.sort_actions()
399
416
  run_name = self.name if self.name else "untitled"
400
417
  run_name = self.validate_function_name(run_name)
401
- exec_string = ''
418
+ exec_str_collection = {}
402
419
 
403
420
  for i in self.stypes:
404
- exec_string += self._generate_function_header(run_name, i)
405
- exec_string += self._generate_function_body(i)
406
-
421
+ if self.script_dict[i]:
422
+ func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
423
+ exec_str_collection[i] = func_str
407
424
  if script_path:
408
- self._write_to_file(script_path, run_name, exec_string)
409
-
410
- return exec_string
411
-
412
- def compile_steps(self, script_path=None):
413
- """
414
- Compile the current script to steps.
415
- :return: {"prep":[], "script":[], "cleanup":[],}.
416
- """
417
- self.sort_actions()
418
- run_name = self.name if self.name else "untitled"
419
- run_name = self.validate_function_name(run_name)
420
- exec_string = ''
421
- steps = {}
422
- for i in self.stypes:
423
- # exec_string += self._generate_function_header(run_name, i)
424
- exec_string += self._generate_function_body(i)
425
+ self._write_to_file(script_path, run_name, exec_str_collection)
425
426
 
426
- if script_path:
427
- self._write_to_file(script_path, run_name, exec_string)
427
+ return exec_str_collection
428
428
 
429
- return exec_string
430
429
 
431
430
 
432
431
  @staticmethod
@@ -447,13 +446,13 @@ class Script(db.Model):
447
446
  configure = [param + f":{param_type}" if not param_type == "any" else "" for param, param_type in
448
447
  config_type.items()]
449
448
 
450
- function_header = f"\n\ndef {run_name}_{stype}("
449
+ function_header = f"def {run_name}_{stype}("
451
450
 
452
451
  if stype == "script":
453
452
  function_header += ", ".join(configure)
454
453
 
455
454
  function_header += "):"
456
- function_header += self.indent(1) + f"global {run_name}_{stype}"
455
+ # function_header += self.indent(1) + f"global {run_name}_{stype}"
457
456
  return function_header
458
457
 
459
458
  def _generate_function_body(self, stype):
@@ -471,21 +470,6 @@ class Script(db.Model):
471
470
  body += self.indent(indent_unit) + return_str
472
471
  return body
473
472
 
474
- # def _generate_function_body(self, stype):
475
- # """
476
- # Generate the function body for each type in stypes.
477
- # """
478
- # steps = []
479
- # indent_unit = 1
480
- #
481
- # for index, action in enumerate(self.script_dict[stype]):
482
- # text, indent_unit = self._process_action(indent_unit, action, index, stype)
483
- # body += text
484
- # return_str, return_list = self.config_return()
485
- # if return_list and stype == "script":
486
- # body += self.indent(indent_unit) + return_str
487
- # return body
488
-
489
473
  def _process_action(self, indent_unit, action, index, stype):
490
474
  """
491
475
  Process each action within the script dictionary.
@@ -631,7 +615,8 @@ class Script(db.Model):
631
615
  else:
632
616
  s.write("deck = None")
633
617
  s.write("\nimport time")
634
- s.write(exec_string)
618
+ for i in exec_string.values():
619
+ s.write(f"\n\n\n{i}")
635
620
 
636
621
 
637
622
  if __name__ == "__main__":
@@ -1,3 +1,4 @@
1
+ import ast
1
2
  import os
2
3
  import csv
3
4
  import threading
@@ -17,25 +18,42 @@ class ScriptRunner:
17
18
  if globals_dict is None:
18
19
  globals_dict = globals()
19
20
  self.globals_dict = globals_dict
20
-
21
- self.stop_event = threading.Event()
21
+ self.pause_event = threading.Event() # A threading event to manage pause/resume
22
+ self.pause_event.set()
23
+ self.stop_pending_event = threading.Event()
24
+ self.stop_current_event = threading.Event()
22
25
  self.is_running = False
23
26
  self.lock = threading.Lock()
24
27
 
28
+ def toggle_pause(self):
29
+ """Toggles between pausing and resuming the script"""
30
+ if self.pause_event.is_set():
31
+ self.pause_event.clear() # Pause the script
32
+ return "Paused"
33
+ else:
34
+ self.pause_event.set() # Resume the script
35
+ return "Resumed"
36
+
25
37
  def reset_stop_event(self):
26
- self.stop_event.clear()
38
+ """Resets the stop event"""
39
+ self.stop_pending_event.clear()
40
+ self.stop_current_event.clear()
27
41
 
28
- def stop_execution(self):
29
- self.stop_event.set()
42
+ def abort_pending(self):
43
+ """Abort the pending iteration after the current is finished"""
44
+ self.stop_pending_event.set()
30
45
  # print("Stop pending tasks")
31
46
 
47
+ def stop_execution(self):
48
+ """Force stop everything, including ongoing tasks."""
49
+ self.stop_current_event.set()
50
+ self.abort_pending()
51
+
32
52
  def run_script(self, script, repeat_count=1, run_name=None, logger=None, socketio=None, config=None, bo_args=None,
33
53
  output_path=""):
34
54
  global deck
35
55
  if deck is None:
36
56
  deck = global_config.deck
37
- exec_string = script.compile()
38
- exec(exec_string)
39
57
  time.sleep(1)
40
58
  with self.lock:
41
59
  if self.is_running:
@@ -50,24 +68,63 @@ class ScriptRunner:
50
68
  thread.start()
51
69
  return thread
52
70
 
53
- def _run_with_stop_check(self, script: Script, repeat_count, run_name, logger, socketio, config, bo_args,
71
+ def execute_function_line_by_line(self, lines:list, section_name, logger, socketio, **kwargs):
72
+ """
73
+ Executes a function defined in a string line by line.
74
+
75
+ :param func_str: The function as a string
76
+ :param kwargs: Arguments to pass to the function
77
+ :return: The final result of the function execution
78
+ """
79
+ global deck
80
+ if deck is None:
81
+ deck = global_config.deck
82
+ # func_str = script.compile()
83
+ # Parse function body from string
84
+
85
+ # Prepare execution environment
86
+ exec_globals = {"deck": deck} # Add required global objects
87
+ exec_locals = {} # Local execution scope
88
+
89
+ # Define function arguments manually in exec_locals
90
+ exec_locals.update(kwargs)
91
+
92
+ # Execute each line dynamically
93
+ for index, line in enumerate(lines):
94
+ if self.stop_current_event.is_set():
95
+ logger.info(f'Stopping execution during {section_name}')
96
+ break
97
+ logger.info(f"Executing: {line}") # Debugging output
98
+ socketio.emit('execution', {'section': f"{section_name}-{index}"})
99
+ # self._emit_progress(socketio, 100)
100
+ exec(line, exec_globals, exec_locals)
101
+ self.pause_event.wait()
102
+
103
+ return exec_locals # Return the 'results' variable
104
+
105
+ def _run_with_stop_check(self, script: Script, repeat_count: int, run_name: str, logger, socketio, config, bo_args,
54
106
  output_path):
55
107
  time.sleep(1)
108
+ func_str = script.compile()
109
+ func_str_lines = script.convert_to_lines(func_str)
56
110
  self._emit_progress(socketio, 1)
57
111
  try:
58
112
  # Run "prep" section once
59
113
  script_dict = script.script_dict
60
- self._run_actions(script_dict.get("prep", []), section_name="prep", run_name=run_name, logger=logger)
114
+ self._run_actions(script_dict.get("prep", []), func_str_lines.get("prep", []), section_name="prep", logger=logger, socketio=socketio)
61
115
  output_list = []
62
116
  _, arg_type = script.config("script")
63
117
  _, return_list = script.config_return()
64
118
  # Run "script" section multiple times
65
119
  if repeat_count:
66
- self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, return_list, run_name, logger, socketio)
120
+ self._run_repeat_section(repeat_count, arg_type, bo_args, output_list, func_str_lines.get("script", []),
121
+ run_name, return_list, logger, socketio)
67
122
  elif config:
68
- self._run_config_section(config, arg_type, output_list, return_list, run_name, logger, socketio)
123
+ self._run_config_section(config, arg_type, output_list, func_str_lines.get("script", []), run_name, logger,
124
+ socketio)
69
125
  # Run "cleanup" section once
70
- self._run_actions(script_dict.get("cleanup", []), section_name="cleanup", run_name=run_name, logger=logger)
126
+ self._run_actions(script_dict.get("cleanup", []), func_str.get("cleanup", []), section_name="cleanup",
127
+ logger=logger, socketio=socketio)
71
128
  # Save results if necessary
72
129
  if output_list:
73
130
  self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
@@ -78,21 +135,14 @@ class ScriptRunner:
78
135
  self.is_running = False # Reset the running flag when done
79
136
  self._emit_progress(socketio, 100)
80
137
 
81
- def _run_actions(self, actions, section_name="", run_name=None, logger=None):
138
+ def _run_actions(self, actions, func_str, section_name="", logger=None, socketio=None):
82
139
  logger.info(f'Executing {section_name} steps') if actions else logger.info(f'No {section_name} steps')
83
- if self.stop_event.is_set():
140
+ if self.stop_pending_event.is_set():
84
141
  logger.info(f"Stopping execution during {section_name} section.")
85
142
  return
86
- # for action in actions:
87
- # if self.stop_event.is_set():
88
- # logger.info(f"Stopping execution during {section_name} section.")
89
- # break
90
- # logger.info(f'Executing {action.get("action", "")} action')
91
- fname = f"{run_name}_{section_name}"
92
- function = self.globals_dict[fname]
93
- function()
94
-
95
- def _run_config_section(self, config, arg_type, output_list, return_list, run_name, logger, socketio):
143
+ self.execute_function_line_by_line(func_str, section_name, logger, socketio)
144
+
145
+ def _run_config_section(self, config, arg_type, output_list, func_str, run_name, logger, socketio):
96
146
  compiled = True
97
147
  for i in config:
98
148
  try:
@@ -104,25 +154,26 @@ class ScriptRunner:
104
154
  if compiled:
105
155
  for i, kwargs in enumerate(config):
106
156
  kwargs = dict(kwargs)
107
- if self.stop_event.is_set():
157
+ if self.stop_pending_event.is_set():
108
158
  logger.info(f'Stopping execution during {run_name}: {i + 1}/{len(config)}')
109
159
  break
110
160
  logger.info(f'Executing {i + 1} of {len(config)} with kwargs = {kwargs}')
111
161
  progress = (i + 1) * 100 / len(config)
112
162
  self._emit_progress(socketio, progress)
113
- fname = f"{run_name}_script"
114
- function = self.globals_dict[fname]
115
- output = function(**kwargs)
163
+ # fname = f"{run_name}_script"
164
+ # function = self.globals_dict[fname]
165
+ output = self.execute_function_line_by_line(func_str, "script", logger, socketio, **kwargs)
116
166
  if output:
117
- kwargs.update(output)
118
- output_list.append(kwargs)
167
+ # kwargs.update(output)
168
+ output_list.append(output)
119
169
 
120
- def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, return_list, run_name, logger, socketio):
170
+ def _run_repeat_section(self, repeat_count, arg_types, bo_args, output_list, func_str, run_name, return_list,
171
+ logger, socketio):
121
172
  if bo_args:
122
173
  logger.info('Initializing optimizer...')
123
174
  ax_client = utils.ax_initiation(bo_args, arg_types)
124
175
  for i in range(int(repeat_count)):
125
- if self.stop_event.is_set():
176
+ if self.stop_pending_event.is_set():
126
177
  logger.info(f'Stopping execution during {run_name}: {i + 1}/{int(repeat_count)}')
127
178
  break
128
179
  logger.info(f'Executing {run_name} experiment: {i + 1}/{int(repeat_count)}')
@@ -132,20 +183,20 @@ class ScriptRunner:
132
183
  try:
133
184
  parameters, trial_index = ax_client.get_next_trial()
134
185
  logger.info(f'Output value: {parameters}')
135
- fname = f"{run_name}_script"
136
- function = self.globals_dict[fname]
137
- output = function(**parameters)
138
- # output = eval(f"{run_name}_script(**{parameters})")
139
- _output = output.copy()
186
+ # fname = f"{run_name}_script"
187
+ # function = self.globals_dict[fname]
188
+ output = self.execute_function_line_by_line(func_str, "script", logger, socketio, **parameters)
189
+
190
+ _output = {key: value for key, value in output.items() if key in return_list}
140
191
  ax_client.complete_trial(trial_index=trial_index, raw_data=_output)
141
192
  output.update(parameters)
142
193
  except Exception as e:
143
194
  logger.info(f'Optimization error: {e}')
144
195
  break
145
196
  else:
146
- fname = f"{run_name}_script"
147
- function = self.globals_dict[fname]
148
- output = function()
197
+ # fname = f"{run_name}_script"
198
+ # function = self.globals_dict[fname]
199
+ output = self.execute_function_line_by_line(func_str, "script", logger, socketio)
149
200
 
150
201
  if output:
151
202
  output_list.append(output)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.20"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivoryos
3
- Version: 0.1.18
3
+ Version: 0.1.20
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
@@ -13,6 +13,9 @@ License-File: LICENSE
13
13
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
14
14
  [![PyPI version](https://img.shields.io/pypi/v/ivoryos)](https://pypi.org/project/ivoryos/)
15
15
  ![License](https://img.shields.io/pypi/l/ivoryos)
16
+ [![YouTube](https://img.shields.io/badge/YouTube-video-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
17
+ [![Research Square](https://img.shields.io/badge/Preprint-blue)](https://www.researchsquare.com/article/rs-5307798/v1)
18
+
16
19
 
17
20
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
18
21
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -24,6 +27,7 @@ License-File: LICENSE
24
27
  - [Installation](#installation)
25
28
  - [Instructions for use](#instructions-for-use)
26
29
  - [Demo](#demo)
30
+ - [Roadmap](#roadmap)
27
31
  - [License](#license)
28
32
 
29
33
  ## Description
@@ -152,9 +156,13 @@ When you run the application for the first time, it will automatically create th
152
156
  - **`ivoryos.db`**: Database file that stores application data locally.
153
157
 
154
158
 
155
- ### Demo video
156
- Intro + Tutorial + Demo with PurPOSE platform
157
- https://youtu.be/dFfJv9I2-1g
159
+ ## Roadmap
160
+
161
+ - [x] Allow plugin pages ✅
162
+ - [x] pause, resume, abort current and pending workflows ✅
163
+ - [ ] snapshot version control
164
+ - [ ] dropdown input
165
+ - [ ] show line number option
158
166
 
159
167
 
160
168
  ## Authors and Acknowledgement
@@ -1,34 +0,0 @@
1
- document.addEventListener("DOMContentLoaded", function() {
2
- var socket = io.connect('http://' + document.domain + ':' + location.port);
3
- socket.on('connect', function() {
4
- console.log('Connected');
5
- });
6
- socket.on('progress', function(data) {
7
- var progress = data.progress;
8
- console.log(progress);
9
- // Update the progress bar's width and appearance
10
- var progressBar = document.getElementById('progress-bar-inner');
11
- progressBar.style.width = progress + '%';
12
- progressBar.setAttribute('aria-valuenow', progress);
13
-
14
- if (progress === 100) {
15
- // Remove animation and set green color when 100% is reached
16
- progressBar.classList.remove('progress-bar-animated');
17
- progressBar.classList.add('bg-success'); // Bootstrap class for green color
18
- }
19
- });
20
- socket.on('log', function(data) {
21
- var logMessage = data.message;
22
- console.log(logMessage);
23
- $('#logging-panel').append(logMessage + "<br>");
24
- $('#logging-panel').scrollTop($('#logging-panel')[0].scrollHeight);
25
- });
26
-
27
- document.getElementById('abort').addEventListener('click', function() {
28
- var confirmation = confirm("Are you sure you want to abort pending actions?");
29
- if (confirmation) {
30
- socket.emit('abort_action');
31
- console.log('Abort action sent to server.');
32
- }
33
- });
34
- });
@@ -1 +0,0 @@
1
- __version__ = "0.1.18"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes