ivoryos 1.3.3__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.3 → ivoryos-1.3.5}/PKG-INFO +41 -6
  2. {ivoryos-1.3.3 → ivoryos-1.3.5}/README.md +30 -5
  3. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/__init__.py +6 -2
  4. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/app.py +2 -1
  5. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/control.py +2 -2
  6. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/design.py +9 -3
  7. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/design_step.py +31 -10
  8. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas_main.html +6 -1
  9. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/server.py +29 -22
  10. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/action_handlers.js +20 -0
  11. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/db_models.py +23 -11
  12. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/global_config.py +11 -0
  13. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/py_to_json.py +19 -4
  14. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/script_runner.py +36 -7
  15. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/task_runner.py +30 -18
  16. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/utils.py +3 -1
  17. ivoryos-1.3.5/ivoryos/version.py +1 -0
  18. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos.egg-info/PKG-INFO +41 -6
  19. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos.egg-info/requires.txt +14 -0
  20. {ivoryos-1.3.3 → ivoryos-1.3.5}/pyproject.toml +12 -1
  21. ivoryos-1.3.3/ivoryos/version.py +0 -1
  22. {ivoryos-1.3.3 → ivoryos-1.3.5}/LICENSE +0 -0
  23. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/config.py +0 -0
  24. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/optimizer/ax_optimizer.py +0 -0
  25. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/optimizer/base_optimizer.py +0 -0
  26. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/optimizer/baybe_optimizer.py +0 -0
  27. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/optimizer/registry.py +0 -0
  28. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/__init__.py +0 -0
  29. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/api/api.py +0 -0
  30. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/auth/__init__.py +0 -0
  31. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/auth/auth.py +0 -0
  32. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/auth/templates/login.html +0 -0
  33. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/auth/templates/signup.html +0 -0
  34. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/__init__.py +0 -0
  35. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/control_file.py +0 -0
  36. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/control_new_device.py +0 -0
  37. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/templates/controllers.html +0 -0
  38. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/templates/controllers_new.html +0 -0
  39. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/control/utils.py +0 -0
  40. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/data/__init__.py +0 -0
  41. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/data/data.py +0 -0
  42. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/data/templates/components/step_card.html +0 -0
  43. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/data/templates/workflow_database.html +0 -0
  44. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/data/templates/workflow_view.html +0 -0
  45. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/__init__.py +0 -0
  46. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/design_file.py +0 -0
  47. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/action_form.html +0 -0
  48. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/actions_panel.html +0 -0
  49. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/autofill_toggle.html +0 -0
  50. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas.html +0 -0
  51. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas_footer.html +0 -0
  52. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/canvas_header.html +0 -0
  53. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/deck_selector.html +0 -0
  54. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/edit_action_form.html +0 -0
  55. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/instruments_panel.html +0 -0
  56. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/drop_modal.html +0 -0
  57. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/json_modal.html +0 -0
  58. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/new_script_modal.html +0 -0
  59. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/rename_modal.html +0 -0
  60. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals/saveas_modal.html +0 -0
  61. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/modals.html +0 -0
  62. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/python_code_overlay.html +0 -0
  63. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/sidebar.html +0 -0
  64. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/components/text_to_code_panel.html +0 -0
  65. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/design/templates/experiment_builder.html +0 -0
  66. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/__init__.py +0 -0
  67. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/execute.py +0 -0
  68. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/execute_file.py +0 -0
  69. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/error_modal.html +0 -0
  70. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/logging_panel.html +0 -0
  71. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/progress_panel.html +0 -0
  72. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/run_panel.html +0 -0
  73. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/run_tabs.html +0 -0
  74. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/tab_bayesian.html +0 -0
  75. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/tab_configuration.html +0 -0
  76. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/components/tab_repeat.html +0 -0
  77. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/execute/templates/experiment_run.html +0 -0
  78. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/library/__init__.py +0 -0
  79. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/library/library.py +0 -0
  80. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/library/templates/library.html +0 -0
  81. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/main/__init__.py +0 -0
  82. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/main/main.py +0 -0
  83. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/main/templates/help.html +0 -0
  84. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/routes/main/templates/home.html +0 -0
  85. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/socket_handlers.py +0 -0
  86. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/favicon.ico +0 -0
  87. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  88. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  89. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/db_delete.js +0 -0
  90. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/overlay.js +0 -0
  91. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/script_metadata.js +0 -0
  92. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/socket_handler.js +0 -0
  93. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/sortable_card.js +0 -0
  94. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/sortable_design.js +0 -0
  95. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/js/ui_state.js +0 -0
  96. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/logo.webp +0 -0
  97. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/static/style.css +0 -0
  98. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/templates/base.html +0 -0
  99. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/__init__.py +0 -0
  100. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/bo_campaign.py +0 -0
  101. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/client_proxy.py +0 -0
  102. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/decorators.py +0 -0
  103. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/form.py +0 -0
  104. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/llm_agent.py +0 -0
  105. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos/utils/serilize.py +0 -0
  106. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos.egg-info/SOURCES.txt +0 -0
  107. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos.egg-info/dependency_links.txt +0 -0
  108. {ivoryos-1.3.3 → ivoryos-1.3.5}/ivoryos.egg-info/top_level.txt +0 -0
  109. {ivoryos-1.3.3 → 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.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
  [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
@@ -28,7 +38,8 @@ Dynamic: license-file
28
38
  [![YouTube](https://img.shields.io/badge/YouTube-tutorial-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
29
39
  [![YouTube](https://img.shields.io/badge/YouTube-demo-red?logo=youtube)](https://youtu.be/flr5ydiE96s)
30
40
  [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
31
- [![Discord](https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2)](https://discord.gg/AX5P9EdGVX)
41
+
42
+ [//]: # ([![Discord]&#40;https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2&#41;]&#40;https://discord.gg/AX5P9EdGVX&#41;)
32
43
 
33
44
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
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
- Login: Create an account (local DB, bcrypt password)
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.
@@ -4,7 +4,8 @@
4
4
  [![YouTube](https://img.shields.io/badge/YouTube-tutorial-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
5
5
  [![YouTube](https://img.shields.io/badge/YouTube-demo-red?logo=youtube)](https://youtu.be/flr5ydiE96s)
6
6
  [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
7
- [![Discord](https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2)](https://discord.gg/AX5P9EdGVX)
7
+
8
+ [//]: # ([![Discord]&#40;https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2&#41;]&#40;https://discord.gg/AX5P9EdGVX&#41;)
8
9
 
9
10
  ![](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos.png)
10
11
  # ivoryOS: interoperable Web UI for self-driving laboratories (SDLs)
@@ -77,11 +78,14 @@ pip install -e .
77
78
  ## Quick start
78
79
  In your SDL script,
79
80
  ```python
81
+ my_robot = Robot()
82
+
80
83
  import ivoryos
81
84
 
82
85
  ivoryos.run(__name__)
83
86
  ```
84
- Login: Create an account (local DB, bcrypt password)
87
+ You can now access the web UI at http://127.0.0.1:8000,
88
+ create an account, login, and start designing workflows!
85
89
 
86
90
  ----
87
91
  ## Features
@@ -103,6 +107,28 @@ Add single or multiple loggers:
103
107
  ivoryos.run(__name__, logger="logger name")
104
108
  ivoryos.run(__name__, logger=["logger 1", "logger 2"])
105
109
  ```
110
+ ### Human-in-the-loop
111
+ Add single or multiple notification handlers for `pause` feature in flow control:
112
+ ```python
113
+
114
+ def slack_bot(msg: str = "Hi"):
115
+ """
116
+ a function that can be used as a notification handler function("msg")
117
+ :param msg: message to send
118
+ """
119
+ from slack_sdk import WebClient
120
+
121
+ slack_token = "your slack token"
122
+ client = WebClient(token=slack_token)
123
+
124
+ my_user_id = "your user id" # replace with your actual Slack user ID
125
+
126
+ client.chat_postMessage(channel=my_user_id, text=msg)
127
+
128
+ import ivoryos
129
+ ivoryos.run(__name__, notification_handler=slack_bot)
130
+ ```
131
+
106
132
  ### Directory Structure
107
133
 
108
134
  Created automatically on first run:
@@ -128,11 +154,10 @@ ivoryos.run(__name__)
128
154
 
129
155
  ## Roadmap
130
156
 
131
- - [x] Allow plugin pages ✅
132
- - [x] pause, resume, abort current and pending workflows ✅
133
157
  - [ ] dropdown input
134
158
  - [ ] snapshot version control
135
159
  - [ ] optimizer-agnostic
160
+ - [ ] prefect compatibility
136
161
  - [ ] check batch-config file compatibility
137
162
 
138
163
  ---
@@ -174,4 +199,4 @@ For an additional perspective related to the development of the tool, please see
174
199
  ```
175
200
  ---
176
201
  ## Acknowledgements
177
- Authors acknowledge Telescope Innovations Corp., Hein Lab members for their valuable suggestions and contributions.
202
+ Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
@@ -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
  ]
@@ -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
- if not has_workflow_phase:
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
  )
@@ -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 %}
@@ -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
- if logger and type(logger) is str:
117
- utils.start_logger(socketio, log_filename=logger_path, logger_name=logger)
118
- elif type(logger) is list:
119
- for log in logger:
120
- utils.start_logger(socketio, log_filename=logger_path, logger_name=log)
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
  }
@@ -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 += " = "
@@ -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
@@ -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