ivoryos 1.3.4__tar.gz → 1.3.5__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 (109) hide show
  1. {ivoryos-1.3.4 → ivoryos-1.3.5}/PKG-INFO +11 -1
  2. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/__init__.py +6 -2
  3. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/control.py +2 -2
  4. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/design.py +9 -3
  5. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/design_step.py +31 -10
  6. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas_main.html +6 -1
  7. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/action_handlers.js +20 -0
  8. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/db_models.py +23 -11
  9. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/py_to_json.py +19 -4
  10. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/script_runner.py +23 -2
  11. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/task_runner.py +28 -17
  12. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/utils.py +2 -1
  13. ivoryos-1.3.5/ivoryos/version.py +1 -0
  14. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos.egg-info/PKG-INFO +11 -1
  15. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos.egg-info/requires.txt +14 -0
  16. {ivoryos-1.3.4 → ivoryos-1.3.5}/pyproject.toml +12 -1
  17. ivoryos-1.3.4/ivoryos/version.py +0 -1
  18. {ivoryos-1.3.4 → ivoryos-1.3.5}/LICENSE +0 -0
  19. {ivoryos-1.3.4 → ivoryos-1.3.5}/README.md +0 -0
  20. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/app.py +0 -0
  21. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/config.py +0 -0
  22. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/optimizer/ax_optimizer.py +0 -0
  23. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/optimizer/base_optimizer.py +0 -0
  24. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  25. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/optimizer/registry.py +0 -0
  26. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/__init__.py +0 -0
  27. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/api/api.py +0 -0
  28. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/auth/__init__.py +0 -0
  29. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/auth/auth.py +0 -0
  30. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/auth/templates/login.html +0 -0
  31. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/auth/templates/signup.html +0 -0
  32. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/__init__.py +0 -0
  33. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/control_file.py +0 -0
  34. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/control_new_device.py +0 -0
  35. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/templates/controllers.html +0 -0
  36. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  37. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/control/utils.py +0 -0
  38. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/data/__init__.py +0 -0
  39. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/data/data.py +0 -0
  40. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/data/templates/components/step_card.html +0 -0
  41. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  42. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/data/templates/workflow_view.html +0 -0
  43. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/__init__.py +0 -0
  44. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/design_file.py +0 -0
  45. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  46. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  47. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  48. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  49. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  50. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  51. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  52. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
  53. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  54. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  55. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  56. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  57. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  58. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  59. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals.html +0 -0
  60. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  61. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  62. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  63. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  64. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/__init__.py +0 -0
  65. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/execute.py +0 -0
  66. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/execute_file.py +0 -0
  67. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  68. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  69. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  70. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  71. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  72. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  73. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  74. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  75. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  76. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/library/__init__.py +0 -0
  77. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/library/library.py +0 -0
  78. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/library/templates/library.html +0 -0
  79. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/main/__init__.py +0 -0
  80. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/main/main.py +0 -0
  81. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/main/templates/help.html +0 -0
  82. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/routes/main/templates/home.html +0 -0
  83. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/server.py +0 -0
  84. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/socket_handlers.py +0 -0
  85. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/favicon.ico +0 -0
  86. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  87. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  88. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/db_delete.js +0 -0
  89. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/overlay.js +0 -0
  90. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/script_metadata.js +0 -0
  91. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/socket_handler.js +0 -0
  92. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/sortable_card.js +0 -0
  93. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/sortable_design.js +0 -0
  94. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/js/ui_state.js +0 -0
  95. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/logo.webp +0 -0
  96. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/static/style.css +0 -0
  97. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/templates/base.html +0 -0
  98. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/__init__.py +0 -0
  99. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/bo_campaign.py +0 -0
  100. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/client_proxy.py +0 -0
  101. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/decorators.py +0 -0
  102. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/form.py +0 -0
  103. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/global_config.py +0 -0
  104. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/llm_agent.py +0 -0
  105. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos/utils/serilize.py +0 -0
  106. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos.egg-info/SOURCES.txt +0 -0
  107. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos.egg-info/dependency_links.txt +0 -0
  108. {ivoryos-1.3.4 → ivoryos-1.3.5}/ivoryos.egg-info/top_level.txt +0 -0
  109. {ivoryos-1.3.4 → ivoryos-1.3.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.4
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
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
@@ -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
  ]
@@ -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
  )
@@ -87,8 +87,11 @@ def experiment_builder():
87
87
 
88
88
  # edit_action_info = session.get("edit_action")
89
89
 
90
-
91
- exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
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
- flash(forms.errors)
59
+ warning = f"Compilation failed: {str(forms.errors)}"
59
60
  utils.post_script_file(script)
60
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
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
- script=script,
65
- buttons_dict=design_buttons)
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
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
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
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
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
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
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)
@@ -31,4 +31,9 @@
31
31
  </div>
32
32
  <div class="python-code-wrapper" id="python-code-wrapper">
33
33
  {% include 'components/python_code_overlay.html' %}
34
- </div>
34
+ </div>
35
+
36
+
37
+ {% if warning %}
38
+ <div id="warning" style="display:none;">{{ warning }}</div>
39
+ {% endif %}
@@ -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
  }
@@ -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
- # Extract function body as source lines
443
- line_collection[stype] = [ast_unparse(node) for node in func_def.body if not isinstance(node, ast.Return)]
444
- # print(line_collection[stype])
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
- func_str = self._generate_function_header(run_name, i) + self._generate_function_body(i)
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
- function_header = f"def {run_name}{script_type}("
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
- return self._process_instrument_action(indent_unit, instrument, action_name, args, save_data)
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 += " = "
@@ -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.Call):
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
- add_card({
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
@@ -1,4 +1,5 @@
1
1
  import ast
2
+ import asyncio
2
3
  import os
3
4
  import csv
4
5
  import threading
@@ -182,7 +183,26 @@ class ScriptRunner:
182
183
  duration = float(duration_str)
183
184
  self.safe_sleep(duration)
184
185
  else:
185
- exec(line, exec_globals, exec_locals)
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)
186
206
  # step.run_error = False
187
207
 
188
208
  except HumanInterventionRequired as e:
@@ -198,6 +218,7 @@ class ScriptRunner:
198
218
 
199
219
  step.run_error = True
200
220
  self.toggle_pause()
221
+ exec_locals.pop("__async_exec_wrapper", None)
201
222
  step.end_time = datetime.now()
202
223
  step.output = exec_locals
203
224
  db.session.commit()
@@ -249,7 +270,7 @@ class ScriptRunner:
249
270
  self._run_actions(script, section_name="cleanup", logger=logger, socketio=socketio,run_id=run_id)
250
271
  # Reset the running flag when done
251
272
  # Save results if necessary
252
- if not script.python_script and output_list:
273
+ if not script.python_script and return_list:
253
274
  filename = self._save_results(run_name, arg_type, return_list, output_list, logger, output_path)
254
275
  self._emit_progress(socketio, 100)
255
276
 
@@ -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
@@ -32,16 +33,15 @@ class TaskRunner:
32
33
  current_status["output"] = "busy"
33
34
  return current_status
34
35
 
35
-
36
36
  if wait:
37
- output = self._run_single_step(component, method, kwargs, current_app)
37
+ output = await self._run_single_step(component, method, kwargs, current_app)
38
38
  else:
39
- print("running with thread")
40
- thread = threading.Thread(
41
- target=self._run_single_step, args=(component, method, kwargs, current_app)
42
- )
43
- thread.start()
44
- time.sleep(0.1)
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
45
45
  output = {"status": "task started", "task_id": global_config.runner_status.get("id")}
46
46
 
47
47
  return output
@@ -60,22 +60,32 @@ class TaskRunner:
60
60
  function_executable = getattr(instrument, method)
61
61
  return function_executable
62
62
 
63
- def _run_single_step(self, component, method, kwargs, current_app=None):
63
+ async def _run_single_step(self, component, method, kwargs, current_app=None):
64
64
  try:
65
65
  function_executable = self._get_executable(component, deck, method)
66
66
  method_name = f"{component}.{method}"
67
67
  except Exception as e:
68
68
  self.lock.release()
69
- return {"status": "error", "msg": e.__str__()}
69
+ return {"status": "error", "msg": str(e)}
70
70
 
71
- # with self.lock:
71
+ # Flask context is NOT async → just use normal "with"
72
72
  with current_app.app_context():
73
- step = SingleStep(method_name=method_name, kwargs=kwargs, run_error=None, start_time=datetime.now())
73
+ step = SingleStep(
74
+ method_name=method_name,
75
+ kwargs=kwargs,
76
+ run_error=None,
77
+ start_time=datetime.now()
78
+ )
74
79
  db.session.add(step)
75
80
  db.session.flush()
76
- global_config.runner_status = {"id":step.id, "type": "task"}
81
+ global_config.runner_status = {"id": step.id, "type": "task"}
82
+
77
83
  try:
78
- output = function_executable(**kwargs)
84
+ if inspect.iscoroutinefunction(function_executable):
85
+ output = await function_executable(**kwargs)
86
+ else:
87
+ output = function_executable(**kwargs)
88
+
79
89
  step.output = output
80
90
  step.end_time = datetime.now()
81
91
  success = True
@@ -87,4 +97,5 @@ class TaskRunner:
87
97
  finally:
88
98
  db.session.commit()
89
99
  self.lock.release()
90
- return dict(success=success, output=output)
100
+
101
+ return dict(success=success, output=output)
@@ -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
- functions[function] = dict(signature=annotation, docstring=docstring)
108
+ coroutine = inspect.iscoroutinefunction(method)
109
+ functions[function] = dict(signature=annotation, docstring=docstring, coroutine=coroutine,)
109
110
 
110
111
  except Exception:
111
112
  pass
@@ -0,0 +1 @@
1
+ __version__ = "1.3.5"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ivoryos
3
- Version: 1.3.4
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
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
@@ -10,6 +10,20 @@ python-dotenv
10
10
  [:python_version < "3.9"]
11
11
  astor
12
12
 
13
+ [dev]
14
+ pytest
15
+
16
+ [doc]
17
+ sphinx
18
+ sphinx-rtd-theme
19
+ sphinxcontrib-httpdomain
20
+
13
21
  [optimizer]
14
22
  ax-platform
15
23
  baybe
24
+
25
+ [optimizer-ax]
26
+ ax-platform
27
+
28
+ [optimizer-baybe]
29
+ baybe
@@ -25,7 +25,18 @@ dependencies = [
25
25
  ]
26
26
 
27
27
  [project.optional-dependencies]
28
- optimizer = ["ax-platform", "baybe"]
28
+ optimizer-ax = ["ax-platform"]
29
+ optimizer-baybe = ["baybe"]
30
+ optimizer = [
31
+ "ax-platform",
32
+ "baybe"
33
+ ]
34
+ doc = [
35
+ "sphinx",
36
+ "sphinx-rtd-theme",
37
+ "sphinxcontrib-httpdomain"
38
+ ]
39
+ dev = ["pytest"]
29
40
 
30
41
  [project.urls]
31
42
  Homepage = "https://gitlab.com/heingroup/ivoryos"
@@ -1 +0,0 @@
1
- __version__ = "1.3.4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes