ivoryos 1.0.9__py3-none-any.whl → 1.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. docs/source/conf.py +84 -0
  2. ivoryos/__init__.py +17 -207
  3. ivoryos/app.py +154 -0
  4. ivoryos/config.py +1 -0
  5. ivoryos/optimizer/ax_optimizer.py +191 -0
  6. ivoryos/optimizer/base_optimizer.py +84 -0
  7. ivoryos/optimizer/baybe_optimizer.py +193 -0
  8. ivoryos/optimizer/nimo_optimizer.py +173 -0
  9. ivoryos/optimizer/registry.py +11 -0
  10. ivoryos/routes/auth/auth.py +43 -14
  11. ivoryos/routes/auth/templates/change_password.html +32 -0
  12. ivoryos/routes/control/control.py +101 -366
  13. ivoryos/routes/control/control_file.py +33 -0
  14. ivoryos/routes/control/control_new_device.py +152 -0
  15. ivoryos/routes/control/templates/controllers.html +193 -0
  16. ivoryos/routes/control/templates/controllers_new.html +112 -0
  17. ivoryos/routes/control/utils.py +40 -0
  18. ivoryos/routes/data/data.py +197 -0
  19. ivoryos/routes/data/templates/components/step_card.html +78 -0
  20. ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +14 -8
  21. ivoryos/routes/data/templates/workflow_view.html +360 -0
  22. ivoryos/routes/design/__init__.py +4 -0
  23. ivoryos/routes/design/design.py +348 -657
  24. ivoryos/routes/design/design_file.py +68 -0
  25. ivoryos/routes/design/design_step.py +171 -0
  26. ivoryos/routes/design/templates/components/action_form.html +53 -0
  27. ivoryos/routes/design/templates/components/actions_panel.html +25 -0
  28. ivoryos/routes/design/templates/components/autofill_toggle.html +10 -0
  29. ivoryos/routes/design/templates/components/canvas.html +5 -0
  30. ivoryos/routes/design/templates/components/canvas_footer.html +9 -0
  31. ivoryos/routes/design/templates/components/canvas_header.html +75 -0
  32. ivoryos/routes/design/templates/components/canvas_main.html +39 -0
  33. ivoryos/routes/design/templates/components/deck_selector.html +10 -0
  34. ivoryos/routes/design/templates/components/edit_action_form.html +53 -0
  35. ivoryos/routes/design/templates/components/info_modal.html +318 -0
  36. ivoryos/routes/design/templates/components/instruments_panel.html +88 -0
  37. ivoryos/routes/design/templates/components/modals/drop_modal.html +17 -0
  38. ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
  39. ivoryos/routes/design/templates/components/modals/new_script_modal.html +17 -0
  40. ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
  41. ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
  42. ivoryos/routes/design/templates/components/modals.html +6 -0
  43. ivoryos/routes/design/templates/components/python_code_overlay.html +56 -0
  44. ivoryos/routes/design/templates/components/sidebar.html +15 -0
  45. ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
  46. ivoryos/routes/design/templates/experiment_builder.html +44 -0
  47. ivoryos/routes/execute/__init__.py +0 -0
  48. ivoryos/routes/execute/execute.py +377 -0
  49. ivoryos/routes/execute/execute_file.py +78 -0
  50. ivoryos/routes/execute/templates/components/error_modal.html +20 -0
  51. ivoryos/routes/execute/templates/components/logging_panel.html +56 -0
  52. ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
  53. ivoryos/routes/execute/templates/components/run_panel.html +9 -0
  54. ivoryos/routes/execute/templates/components/run_tabs.html +60 -0
  55. ivoryos/routes/execute/templates/components/tab_bayesian.html +520 -0
  56. ivoryos/routes/execute/templates/components/tab_configuration.html +383 -0
  57. ivoryos/routes/execute/templates/components/tab_repeat.html +18 -0
  58. ivoryos/routes/execute/templates/experiment_run.html +30 -0
  59. ivoryos/routes/library/__init__.py +0 -0
  60. ivoryos/routes/library/library.py +157 -0
  61. ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +32 -23
  62. ivoryos/routes/main/main.py +31 -3
  63. ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
  64. ivoryos/server.py +180 -0
  65. ivoryos/socket_handlers.py +52 -0
  66. ivoryos/static/ivoryos_logo.png +0 -0
  67. ivoryos/static/js/action_handlers.js +384 -0
  68. ivoryos/static/js/db_delete.js +23 -0
  69. ivoryos/static/js/script_metadata.js +39 -0
  70. ivoryos/static/js/socket_handler.js +40 -5
  71. ivoryos/static/js/sortable_design.js +107 -56
  72. ivoryos/static/js/ui_state.js +114 -0
  73. ivoryos/templates/base.html +67 -8
  74. ivoryos/utils/bo_campaign.py +180 -3
  75. ivoryos/utils/client_proxy.py +267 -36
  76. ivoryos/utils/db_models.py +300 -65
  77. ivoryos/utils/decorators.py +34 -0
  78. ivoryos/utils/form.py +63 -29
  79. ivoryos/utils/global_config.py +34 -1
  80. ivoryos/utils/nest_script.py +314 -0
  81. ivoryos/utils/py_to_json.py +295 -0
  82. ivoryos/utils/script_runner.py +599 -165
  83. ivoryos/utils/serilize.py +201 -0
  84. ivoryos/utils/task_runner.py +71 -21
  85. ivoryos/utils/utils.py +50 -6
  86. ivoryos/version.py +1 -1
  87. ivoryos-1.4.4.dist-info/METADATA +263 -0
  88. ivoryos-1.4.4.dist-info/RECORD +119 -0
  89. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/WHEEL +1 -1
  90. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info}/top_level.txt +1 -0
  91. tests/unit/test_type_conversion.py +42 -0
  92. tests/unit/test_util.py +3 -0
  93. ivoryos/routes/control/templates/control/controllers.html +0 -78
  94. ivoryos/routes/control/templates/control/controllers_home.html +0 -55
  95. ivoryos/routes/control/templates/control/controllers_new.html +0 -89
  96. ivoryos/routes/database/database.py +0 -306
  97. ivoryos/routes/database/templates/database/step_card.html +0 -7
  98. ivoryos/routes/database/templates/database/workflow_view.html +0 -130
  99. ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
  100. ivoryos/routes/design/templates/design/experiment_run.html +0 -558
  101. ivoryos-1.0.9.dist-info/METADATA +0 -218
  102. ivoryos-1.0.9.dist-info/RECORD +0 -61
  103. /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
  104. /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
  105. /ivoryos/routes/{database → data}/__init__.py +0 -0
  106. /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
  107. {ivoryos-1.0.9.dist-info → ivoryos-1.4.4.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,263 @@
1
+ Metadata-Version: 2.4
2
+ Name: ivoryos
3
+ Version: 1.4.4
4
+ Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
+ Author-email: Ivory Zhang <ivoryzhang@chem.ubc.ca>
6
+ License: MIT
7
+ Project-URL: Homepage, https://gitlab.com/heingroup/ivoryos
8
+ Requires-Python: >=3.7
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: bcrypt
12
+ Requires-Dist: Flask[async]
13
+ Requires-Dist: Flask-Login
14
+ Requires-Dist: Flask-Session
15
+ Requires-Dist: Flask-SocketIO
16
+ Requires-Dist: Flask-SQLAlchemy
17
+ Requires-Dist: Flask-WTF
18
+ Requires-Dist: SQLAlchemy-Utils
19
+ Requires-Dist: python-dotenv
20
+ Requires-Dist: pandas
21
+ Requires-Dist: astor; python_version < "3.9"
22
+ Provides-Extra: optimizer-ax
23
+ Requires-Dist: ax-platform; extra == "optimizer-ax"
24
+ Provides-Extra: optimizer-baybe
25
+ Requires-Dist: baybe; extra == "optimizer-baybe"
26
+ Provides-Extra: optimizer-nimo
27
+ Requires-Dist: nimo; extra == "optimizer-nimo"
28
+ Provides-Extra: optimizers
29
+ Requires-Dist: ax-platform>=1.1.2; extra == "optimizers"
30
+ Requires-Dist: baybe>=0.14.0; extra == "optimizers"
31
+ Requires-Dist: nimo; extra == "optimizers"
32
+ Provides-Extra: doc
33
+ Requires-Dist: sphinx; extra == "doc"
34
+ Requires-Dist: sphinx-rtd-theme; extra == "doc"
35
+ Requires-Dist: sphinxcontrib-httpdomain; extra == "doc"
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ [![Documentation Status](https://readthedocs.org/projects/ivoryos/badge/?version=latest)](https://ivoryos.readthedocs.io/en/latest/?badge=latest)
41
+ [![PyPI version](https://img.shields.io/pypi/v/ivoryos)](https://pypi.org/project/ivoryos/)
42
+ ![License](https://img.shields.io/pypi/l/ivoryos)
43
+ [![YouTube](https://img.shields.io/badge/YouTube-tutorial-red?logo=youtube)](https://youtu.be/dFfJv9I2-1g)
44
+ [![YouTube](https://img.shields.io/badge/YouTube-demo-red?logo=youtube)](https://youtu.be/flr5ydiE96s)
45
+ [![Published](https://img.shields.io/badge/Nature_Comm.-paper-blue)](https://www.nature.com/articles/s41467-025-60514-w)
46
+
47
+ [//]: # ([![Discord]&#40;https://img.shields.io/discord/1313641159356059770?label=Discord&logo=discord&color=5865F2&#41;]&#40;https://discord.gg/AX5P9EdGVX&#41;)
48
+
49
+ ![ivoryos_logo.png](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/ivoryos_logo.png)
50
+
51
+ # [IvoryOS](https://ivoryos.ai): interoperable orchestrator for self-driving laboratories (SDLs)
52
+
53
+ A **plug-and-play** web interface for flexible, modular SDLs —
54
+ you focus on developing protocols, IvoryOS handles the rest.
55
+
56
+ ![code_launch_design.png](https://gitlab.com/heingroup/ivoryos/raw/main/docs/source/_static/code_launch_design.png)
57
+
58
+ ---
59
+
60
+ ## Table of Contents
61
+ - [What IvoryOS does](#what-ivoryos-does)
62
+ - [System requirements](#system-requirements)
63
+ - [Installation](#installation)
64
+ - [Features](#features)
65
+ - [Demo](#demo)
66
+ - [Roadmap](#roadmap)
67
+ - [Contributing](#contributing)
68
+ - [Acknowledgements](#acknowledgements)
69
+
70
+ ---
71
+ ## What IvoryOS Does
72
+ - Turns Python modules into UIs by dynamically inspecting your hardware APIs, functions, and workflows.
73
+ - Standardizes optimization inputs/outputs, making any optimizer plug-and-play.
74
+ - Provides a visual workflow builder for designing and running experiments.
75
+ - Adds natural-language control for creating and executing workflows, see [IvoryOS MCP](https://gitlab.com/heingroup/ivoryos-suite/ivoryos-mcp) for more details.
76
+
77
+ ----
78
+ ## System Requirements
79
+
80
+ **Platforms:** Compatible with Linux, macOS, and Windows (developed/tested on Windows).
81
+ **Python:**
82
+ - Recommended: Python ≥3.10
83
+ - Minimum: Python ≥3.7 (without Ax optimizer support)
84
+
85
+ **Core Dependencies:**
86
+ <details>
87
+ <summary>Click to expand</summary>
88
+
89
+ - bcrypt~=4.0
90
+ - Flask-Login~=0.6
91
+ - Flask-Session~=0.8
92
+ - Flask-SocketIO~=5.3
93
+ - Flask-SQLAlchemy~=3.1
94
+ - SQLAlchemy-Utils~=0.41
95
+ - Flask-WTF~=1.2
96
+ - python-dotenv==1.0.1
97
+ - pandas
98
+
99
+ **Optional:**
100
+ - ax-platform==1.1.2
101
+ - baybe==0.14.0
102
+ - nimo
103
+ - slack-sdk
104
+ </details>
105
+
106
+ ---
107
+
108
+
109
+ ## Installation
110
+ From PyPI:
111
+ ```bash
112
+ pip install ivoryos
113
+ ```
114
+
115
+ [//]: # (From source:)
116
+
117
+ [//]: # (```bash)
118
+
119
+ [//]: # (git clone https://gitlab.com/heingroup/ivoryos.git)
120
+
121
+ [//]: # (cd ivoryos)
122
+
123
+ [//]: # (pip install -e .)
124
+
125
+ [//]: # (```)
126
+
127
+
128
+ ## Quick start
129
+ In your script, where you initialize or import your robot:
130
+ ```python
131
+ my_robot = Robot()
132
+
133
+ import ivoryos
134
+
135
+ ivoryos.run(__name__)
136
+ ```
137
+ Then run the script and visit `http://localhost:8000` in your browser.
138
+ Use `admin` for both username and password, and start building workflows!
139
+
140
+ ----
141
+ ## Features
142
+ ### Direct control:
143
+ direct function calling _Devices_ tab
144
+ ### Workflows
145
+ - **Design Editor**: drag/add function to canvas in _Design_ tab. click `Compile and Run` button to go to the execution configuration page
146
+ - **Execution Config**: configure iteration methods and parameters in _Compile/Run_ tab.
147
+ - **Design Library**: manage workflow scripts in _Library_ tab.
148
+ - **Workflow Data**: Execution records are in _Data_ tab.
149
+ ### Offline mode
150
+ after one successful connection, a blueprint will be automatically saved and made accessible without hardware connection. In a new Python script in the same directory, use `ivoryos.run()` to start offline mode.
151
+
152
+
153
+
154
+ ### Logging
155
+ Add single or multiple loggers:
156
+ ```python
157
+ ivoryos.run(__name__, logger="logger name")
158
+ ivoryos.run(__name__, logger=["logger 1", "logger 2"])
159
+ ```
160
+ ### Human-in-the-loop
161
+ Use `pause` in flow control to pause the workflow and send a notification with custom message handler(s).
162
+ When run into `pause`, it will pause, send a message, and wait for human's response. Example of a Slack bot:
163
+ ```python
164
+
165
+ def slack_bot(msg: str = "Hi"):
166
+ """
167
+ a function that can be used as a notification handler function("msg")
168
+ :param msg: message to send
169
+ """
170
+ from slack_sdk import WebClient
171
+
172
+ slack_token = "your slack token"
173
+ client = WebClient(token=slack_token)
174
+
175
+ my_user_id = "your user id" # replace with your actual Slack user ID
176
+
177
+ client.chat_postMessage(channel=my_user_id, text=msg)
178
+
179
+ import ivoryos
180
+ ivoryos.run(__name__, notification_handler=slack_bot)
181
+ ```
182
+
183
+ ### Directory Structure
184
+
185
+ Created automatically in the same working directory on the first run:
186
+ <details>
187
+ <summary>click to see the data folder structure</summary>
188
+
189
+ - **`ivoryos_data/`**:
190
+ - **`config_csv/`**: Batch configuration `csv`
191
+ - **`pseudo_deck/`**: Offline deck `.pkl`
192
+ - **`results/`**: Execution results
193
+ - **`scripts/`**: Compiled workflows Python scripts
194
+ - **`default.log`**: Application logs
195
+ - **`ivoryos.db`**: Local database
196
+ </details>
197
+
198
+ ---
199
+
200
+ ## Demo
201
+ Online demo at [demo.ivoryos.ai](https://demo.ivoryos.ai).
202
+ Local version in [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/community/examples/abstract_sdl_example/abstract_sdl.py)
203
+
204
+ ---
205
+
206
+ ## Roadmap
207
+
208
+ - [ ] dropdown input
209
+ - [ ] snapshot version control
210
+ - [ ] check batch-config file compatibility
211
+
212
+ ---
213
+
214
+ ## Contributing
215
+
216
+ We welcome all contributions — from core improvements to new drivers, plugins, and real-world use cases.
217
+ See `CONTRIBUTING.md` for details and let us know you're interested: https://forms.gle/fPSvw5LEGrweUQUH8
218
+
219
+ ---
220
+
221
+ ## Citing
222
+
223
+ <details>
224
+ <summary>Click to see citations</summary>
225
+
226
+ If you find this project useful, please consider citing the following manuscript:
227
+
228
+ > Zhang, W., Hao, L., Lai, V. et al. [IvoryOS: an interoperable web interface for orchestrating Python-based self-driving laboratories.](https://www.nature.com/articles/s41467-025-60514-w) Nat Commun 16, 5182 (2025).
229
+
230
+ ```bibtex
231
+ @article{zhang_et_al_2025,
232
+ author = {Wenyu Zhang and Lucy Hao and Veronica Lai and Ryan Corkery and Jacob Jessiman and Jiayu Zhang and Junliang Liu and Yusuke Sato and Maria Politi and Matthew E. Reish and Rebekah Greenwood and Noah Depner and Jiyoon Min and Rama El-khawaldeh and Paloma Prieto and Ekaterina Trushina and Jason E. Hein},
233
+ title = {{IvoryOS}: an interoperable web interface for orchestrating {Python-based} self-driving laboratories},
234
+ journal = {Nature Communications},
235
+ year = {2025},
236
+ volume = {16},
237
+ number = {1},
238
+ pages = {5182},
239
+ doi = {10.1038/s41467-025-60514-w},
240
+ url = {https://doi.org/10.1038/s41467-025-60514-w}
241
+ }
242
+ ```
243
+
244
+ For an additional perspective related to the development of the tool, please see:
245
+
246
+ > Zhang, W., Hein, J. [Behind IvoryOS: Empowering Scientists to Harness Self-Driving Labs for Accelerated Discovery](https://communities.springernature.com/posts/behind-ivoryos-empowering-scientists-to-harness-self-driving-labs-for-accelerated-discovery). Springer Nature Research Communities (2025).
247
+
248
+ ```bibtex
249
+ @misc{zhang_hein_2025,
250
+ author = {Wenyu Zhang and Jason Hein},
251
+ title = {Behind {IvoryOS}: Empowering Scientists to Harness Self-Driving Labs for Accelerated Discovery},
252
+ howpublished = {Springer Nature Research Communities},
253
+ year = {2025},
254
+ month = {Jun},
255
+ day = {18},
256
+ url = {https://communities.springernature.com/posts/behind-ivoryos-empowering-scientists-to-harness-self-driving-labs-for-accelerated-discovery}
257
+ }
258
+ ```
259
+ </details>
260
+
261
+ ---
262
+ ## Acknowledgements
263
+ Authors acknowledge Telescope Innovations Corp., UBC Hein Lab, and Acceleration Consortium members for their valuable suggestions and contributions.
@@ -0,0 +1,119 @@
1
+ docs/source/conf.py,sha256=hETfkDMTdj-NYgSI_671gVOVd7iwqU3tEeXnQe2xEWs,2004
2
+ ivoryos/__init__.py,sha256=gEvBO2y5TRq06Itjjej3iAcq73UsihqKPWcb2HykPwM,463
3
+ ivoryos/app.py,sha256=TG-RctBKj8VVOeXciYZ2s_ehP01PACwvLRfl2f3vF6w,5561
4
+ ivoryos/config.py,sha256=y3RxNjiIola9tK7jg-mHM8EzLMwiLwOzoisXkDvj0gA,2174
5
+ ivoryos/server.py,sha256=9zw3c4IGj3DCtjRzEdEG3N4uxAtRZA1fowQGW1d_y94,7208
6
+ ivoryos/socket_handlers.py,sha256=KSh8TxbLFTcmaa-lEb5rKJvOYjKgnGTtaKZST8Wh3b8,1326
7
+ ivoryos/version.py,sha256=6stz_YFBRP_lguydVylsTgSmj3VTb6WFq8Sp45WYQyY,22
8
+ ivoryos/optimizer/ax_optimizer.py,sha256=43w4ollMfWn1DPB4jJ15pc13XxIktWjlxHn9NX339go,8566
9
+ ivoryos/optimizer/base_optimizer.py,sha256=m_wXan_xPueUWNQi3-561Pe_060tsKuUcCTOOu0qu8s,2652
10
+ ivoryos/optimizer/baybe_optimizer.py,sha256=grXmePNuTQsCgRDhJGsAWQuSvvt0hqjhvzpyrZKYjD0,8502
11
+ ivoryos/optimizer/nimo_optimizer.py,sha256=ZevLJUhJKpwatyiOSmPEz3JAUZTdZNKEc_J7z1fAvVQ,7303
12
+ ivoryos/optimizer/registry.py,sha256=dLMo5cszcwa06hfBxdINQIGpkHtRe5-J3J7t76Jq6X0,306
13
+ ivoryos/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ ivoryos/routes/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ ivoryos/routes/auth/auth.py,sha256=AkYnyI4u5ySshpwPK8Z0KVEbR9X1BGdrqDu_IDea-AM,4348
16
+ ivoryos/routes/auth/templates/change_password.html,sha256=GfnytIPyhiLgnc2FzX-aXad1IV2I0nE3A-BQ4KVdaA0,1517
17
+ ivoryos/routes/auth/templates/login.html,sha256=WSRrKbdM_oobqSXFRTo-j9UlOgp6sYzS9tm7TqqPULI,1207
18
+ ivoryos/routes/auth/templates/signup.html,sha256=b5LTXtpfTSkSS7X8u1ldwQbbgEFTk6UNMAediA5BwBY,1465
19
+ ivoryos/routes/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ ivoryos/routes/control/control.py,sha256=0g23PoPPoTeMD7Szm3UAW10sEMLpSD-iTfASAZE4iIE,6442
21
+ ivoryos/routes/control/control_file.py,sha256=3fQ9R8EcdqKs_hABn2EqRAB1xC2DHAT_q_pwsMIDDQI,864
22
+ ivoryos/routes/control/control_new_device.py,sha256=oHbNUjTyv9yh-4FNTxkJqunaZbyH0RiiLoqIjLYUTEQ,5725
23
+ ivoryos/routes/control/utils.py,sha256=XlhhqAtOj7n3XfHPDxJ8TvCV2K2I2IixB0CBkl1QeQc,1242
24
+ ivoryos/routes/control/templates/controllers.html,sha256=5hF3zcx5Rpy0Zaoq-5YGrR_TvPD9MGIa30fI4smEii0,9702
25
+ ivoryos/routes/control/templates/controllers_new.html,sha256=eVeLABT39DWOIYrwWClw7sAD3lCoAGCznygPgFbQoRc,5945
26
+ ivoryos/routes/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ ivoryos/routes/data/data.py,sha256=MR7Vu7HB9dWjVr3cPwJbfIvTWs1Rqh2uXOPWIEFivzs,6399
28
+ ivoryos/routes/data/templates/workflow_database.html,sha256=ofvHcovpwmJXo1SFiSrL8I9kLU_3U1UxsJUUrQ2CJUU,4878
29
+ ivoryos/routes/data/templates/workflow_view.html,sha256=NljmhBVBUYfoOkuUWpVYUYsQkBFPbKlmzYFmFY7KHvY,13011
30
+ ivoryos/routes/data/templates/components/step_card.html,sha256=fxTg-1_Vw8b8QzAUXsPMfFgTmcHo8esAdLTUvK1cKVI,3726
31
+ ivoryos/routes/design/__init__.py,sha256=zS3HXKaw0ALL5n6t_W1rUz5Uj5_tTQ-Y1VMXyzewvR0,113
32
+ ivoryos/routes/design/design.py,sha256=n7768F60Nx04ST7yZSoKiNsoRFCwKjFsZvdzveoFLq0,20042
33
+ ivoryos/routes/design/design_file.py,sha256=MVIc5uGSaGxZhs86hfPjX2n0iy1OcXeLq7b9Ucdg4VQ,2115
34
+ ivoryos/routes/design/design_step.py,sha256=W2mFKMOkbSDBk4Gx9wRYcwjeru2mj-ub1l_K4hPRuZY,6031
35
+ ivoryos/routes/design/templates/experiment_builder.html,sha256=B5NHnOfTbC8xIfmhIQZlQn1_eTd1Ld6c_q5xoMed_Hs,1747
36
+ ivoryos/routes/design/templates/components/action_form.html,sha256=kXJOrJLbFsMHHWVSuMQHpt1xFrUMnwgzTG8e6Qfn0Cg,3042
37
+ ivoryos/routes/design/templates/components/actions_panel.html,sha256=jHTR58saTUIZInBdC-vLc1ZTbStLiULeWbupjB4hQzo,977
38
+ ivoryos/routes/design/templates/components/autofill_toggle.html,sha256=CRVQUHoQT7sOSO5-Vax54ImHdT4G_mEgqR5OQkeUwK8,617
39
+ ivoryos/routes/design/templates/components/canvas.html,sha256=bKLCJaG1B36Yy9Vsnz4P5qiX4BPdfaGe9JeQQzu9rsI,268
40
+ ivoryos/routes/design/templates/components/canvas_footer.html,sha256=5VRRacMZbzx0hUej0NPP-PmXM_AtUqduHzDS7a60cQY,435
41
+ ivoryos/routes/design/templates/components/canvas_header.html,sha256=7iIzLDGHX7MnmBbf98nWtLDprbeIgoNV4dJUO1zE4Tc,3598
42
+ ivoryos/routes/design/templates/components/canvas_main.html,sha256=nLEtp3U2YtfJwob1kR8ua8-UVdu9hwc6z1L5UMNVz8c,1524
43
+ ivoryos/routes/design/templates/components/deck_selector.html,sha256=ryTRpljYezo0AzGLCJu_qOMokjjnft3GIxddmNGtBA0,657
44
+ ivoryos/routes/design/templates/components/edit_action_form.html,sha256=1xL0wueewp_Hdua9DzYJBry09Qcchh4XgGmt4qPCwnc,2782
45
+ ivoryos/routes/design/templates/components/info_modal.html,sha256=ifetOzL124a7NZFsJhjmm04fTaKyibHJ32I0ek0zPoA,19156
46
+ ivoryos/routes/design/templates/components/instruments_panel.html,sha256=tRKd-wOqKjaMJCLuGgRmHtxIgSjklhBkuX8arm5aTCU,4268
47
+ ivoryos/routes/design/templates/components/modals.html,sha256=6Dl8I8oD4ln7kK8C5e92pFVVH5KDte-vVTL0U_6NSTg,306
48
+ ivoryos/routes/design/templates/components/python_code_overlay.html,sha256=Iyk-wOzk1x2997kqGt3uJONh_rpi5sCUs7VCRDg8Q9U,2150
49
+ ivoryos/routes/design/templates/components/sidebar.html,sha256=A6dRo53zIB6QJVrRLJcBZHUNJ3qpYPnR3kWxM8gTkjw,501
50
+ ivoryos/routes/design/templates/components/text_to_code_panel.html,sha256=d-omdXk-PXAR5AyWPr4Rc4pqsebZOiTiMrnz3pPCnUY,1197
51
+ ivoryos/routes/design/templates/components/modals/drop_modal.html,sha256=LPxcycSiBjdQbajYOegjMQEi7ValcaczGoWmW8Sz3Ms,779
52
+ ivoryos/routes/design/templates/components/modals/json_modal.html,sha256=R-SeEdhtuDVbwOWYYH_hCdpul7y4ybCWoNwVIO5j49s,1122
53
+ ivoryos/routes/design/templates/components/modals/new_script_modal.html,sha256=pxZdWWDgI52VsTFzz6pIM9m_dTwR6jARcvCYQ6fV3Lc,937
54
+ ivoryos/routes/design/templates/components/modals/rename_modal.html,sha256=40rLNF9JprdXekB3mv_S3OdqVuQYOe-BZSCgOnIkxJQ,1202
55
+ ivoryos/routes/design/templates/components/modals/saveas_modal.html,sha256=N5PEqUuK3qxDFbtDKFnzHwhLarQLPHiX-XQAdQPL1AU,1555
56
+ ivoryos/routes/execute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
+ ivoryos/routes/execute/execute.py,sha256=hf64lnhiFxjAXy7b5XsFbORKAlkFTQEtqbfrhdYplXk,14603
58
+ ivoryos/routes/execute/execute_file.py,sha256=TelWYV295p4ZPhkUDJSVxfYROfVaKodEmDPTS2plQHI,2816
59
+ ivoryos/routes/execute/templates/experiment_run.html,sha256=fvHRrG5ii1v5zPHL9N_ljDGHUo3t3amiproVPtnOFVE,944
60
+ ivoryos/routes/execute/templates/components/error_modal.html,sha256=5Dmp9V0Ys6781Y-pKn_mc4N9J46c8EwIkjkHX22xCsw,1025
61
+ ivoryos/routes/execute/templates/components/logging_panel.html,sha256=-elcawnE4CaumyivzxaHW3S5xSv5CgZtXt0OHljlits,2554
62
+ ivoryos/routes/execute/templates/components/progress_panel.html,sha256=-nX76aFLxSOiYgI1xMjznC9rDYF-Vb92TmfjXYpBtps,1323
63
+ ivoryos/routes/execute/templates/components/run_panel.html,sha256=CmK-LYJ4K6RonHn6l9eJkqRw0XQizThOugxiXZonSDs,373
64
+ ivoryos/routes/execute/templates/components/run_tabs.html,sha256=HZUBQxJIigp9DgMvcxZOoR_pqz4ZW9kpxhbCjW6_dRg,2724
65
+ ivoryos/routes/execute/templates/components/tab_bayesian.html,sha256=oxtXWb5rUo88yTyyB9dZNxfzi0YanKy8NS9SBuiZKbI,24864
66
+ ivoryos/routes/execute/templates/components/tab_configuration.html,sha256=uZQfi8QLIGbCvgQR2M-fuFAjW4id9ANlYgBUbTJfaZw,14878
67
+ ivoryos/routes/execute/templates/components/tab_repeat.html,sha256=s8Q9Vuztf_h0vy97CBjHwgMdbaLQwall6StVdbL6FY8,989
68
+ ivoryos/routes/library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
+ ivoryos/routes/library/library.py,sha256=QKeP34COssXDipn3d9yEn7pVPYbPb-eFg_X0C8JTvVQ,5387
70
+ ivoryos/routes/library/templates/library.html,sha256=kMaQphkoGQdxIRcQmVcEIn8eghuv2AAClHpo7jGDVv8,4021
71
+ ivoryos/routes/main/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
+ ivoryos/routes/main/main.py,sha256=3hM85DB0edtjTh0YYVqwfWe0-gL2dX7F6HooTrCRvvs,1856
73
+ ivoryos/routes/main/templates/help.html,sha256=IOktMEsOPk0SCiMBXZ4mpffClERAyX8W82fel71M3M0,9370
74
+ ivoryos/routes/main/templates/home.html,sha256=BDvwkVthxniQ157H6E2hgYHT1Vv1GVBwu6dQejtzwoo,4633
75
+ ivoryos/static/favicon.ico,sha256=RhlrPtfITOkzC9BjP1UB1V5L9Oyp6NwNtWeMcGOnpyc,15406
76
+ ivoryos/static/ivoryos_logo.png,sha256=I-1POqhLdPveruxsFbKhKUKAXspHfyxvowpCRFxEzvc,11656
77
+ ivoryos/static/logo.webp,sha256=lXgfQR-4mHTH83k7VV9iB54-oC2ipe6uZvbwdOnLETc,14974
78
+ ivoryos/static/style.css,sha256=zQVx35A5g6JMJ-K84-6fSKtzXGjp_p5ZVG6KLHPM2IE,4021
79
+ ivoryos/static/gui_annotation/Slide1.png,sha256=Lm4gdOkUF5HIUFaB94tl6koQVkzpitKj43GXV_XYMMc,121727
80
+ ivoryos/static/gui_annotation/Slide2.PNG,sha256=z3wQ9oVgg4JTWVLQGKK_KhtepRHUYP1e05XUWGT2A0I,118761
81
+ ivoryos/static/js/action_handlers.js,sha256=VC_kHFk0KKE7Q20PNRSIyxUDgrl_-54Tr2w55JvlhCU,11512
82
+ ivoryos/static/js/db_delete.js,sha256=l67fqUaN_FVDaL7v91Hd7LyRbxnqXx9nyjF34-7aewY,561
83
+ ivoryos/static/js/overlay.js,sha256=dPxop19es0E0ZUSY3d_4exIk7CJuQEnlW5uTt5fZfzI,483
84
+ ivoryos/static/js/script_metadata.js,sha256=m8VYZ8OGT2oTx1kXMXq60bKQI9WCbJNkzcFDzLvRuGc,1188
85
+ ivoryos/static/js/socket_handler.js,sha256=5MGLj-nNA3053eBcko4MZAB5zq2mkgr1g__c_rr3cE8,6849
86
+ ivoryos/static/js/sortable_card.js,sha256=ifmlGe3yy0U_KzMphV4ClRhK2DLOvkELYMlq1vECuac,807
87
+ ivoryos/static/js/sortable_design.js,sha256=ZezDxKtFxqmqSLfFy-HJ61LvQSXL36imYud6TIREs3U,5503
88
+ ivoryos/static/js/ui_state.js,sha256=XYsOcfGlduqLlqHySvPrRrR50CiAsml51duqneigsRY,3368
89
+ ivoryos/templates/base.html,sha256=uJ5lcYUCImiOasyP3g1-dQ-9Fdkff0Re6uUEGud2ySo,12011
90
+ ivoryos/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
+ ivoryos/utils/bo_campaign.py,sha256=Z4WXJwbf1Hhg1dtlvhWHncBiq9Cuaf0BiqR60bWy36U,10038
92
+ ivoryos/utils/client_proxy.py,sha256=74G3HAuq50iEHkSvlMZFmQaukm613FbRgOdzO_T3dMg,10191
93
+ ivoryos/utils/db_models.py,sha256=3cO5EcekYMDQQl3LujXR_8i0Lrgvs34BX34KUtafqwY,37994
94
+ ivoryos/utils/decorators.py,sha256=pcD8WijFjNfkNvr-wmJSWTHn_OqCOCpAO-7w_qg9_xM,1051
95
+ ivoryos/utils/form.py,sha256=MNIvmx4RhDukFACwyrXS-88tBeMKeNdxVba_KzOab_Y,23469
96
+ ivoryos/utils/global_config.py,sha256=leYoEXvAS0AH4xQpYsqu4HI9CJ9-wiLM-pIh_bEG4Ak,3087
97
+ ivoryos/utils/llm_agent.py,sha256=-lVCkjPlpLues9sNTmaT7bT4sdhWvV2DiojNwzB2Lcw,6422
98
+ ivoryos/utils/nest_script.py,sha256=kc4V0cBca5TviqQuquMFBF8Q5IjqKqj2R6g-_N6gMak,10645
99
+ ivoryos/utils/py_to_json.py,sha256=ZtejHgwdEAUCVVMYeVNR8G7ceLINue294q6WpiJ6jn0,9734
100
+ ivoryos/utils/script_runner.py,sha256=jjgloXAbcZprZmjeS-IXnw-KCIFQWKjPYqsv_PrN9Qs,32249
101
+ ivoryos/utils/serilize.py,sha256=lkBhkz8r2bLmz2_xOb0c4ptSSOqjIu6krj5YYK4Nvj8,6784
102
+ ivoryos/utils/task_runner.py,sha256=AnY0DwS5ytU5f7OfCFlNJ72yAonA3tla4UhC7MOM3mw,4818
103
+ ivoryos/utils/utils.py,sha256=n0tQKIsEIVqrWEjN8vJHmobDcZOPmvyrHxoBAdaQr4g,15414
104
+ ivoryos-1.4.4.dist-info/licenses/LICENSE,sha256=p2c8S8i-8YqMpZCJnadLz1-ofxnRMILzz6NCMIypRag,1084
105
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
106
+ tests/conftest.py,sha256=u2sQ6U-Lghyl7et1Oz6J2E5VZ47VINKcjRM_2leAE2s,3627
107
+ tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
108
+ tests/integration/test_route_auth.py,sha256=l3ZDqr0oiCWS3yYSXGK5yMP6qI2t7Sv5I9zoYTkiyQU,2754
109
+ tests/integration/test_route_control.py,sha256=YYIll84bTUEKiAxFiFSz6LF3fTldPNfCtHs0IR3mSdM,3935
110
+ tests/integration/test_route_database.py,sha256=mS026W_hEuCTMpSkdRWvM-f4MYykK_6nRDJ4K5a7QA0,2342
111
+ tests/integration/test_route_design.py,sha256=PJAvGRiCY6B53Pu1v5vPAVHHsuaqRmRKk2eesSNshLU,1157
112
+ tests/integration/test_route_main.py,sha256=bmuf8Y_9CRWhiLLf4up11ltEd5YCdsLx6I-o26VGDEw,1228
113
+ tests/integration/test_sockets.py,sha256=4ZyFyExm7a-DYzVqpzEONpWeb1a0IT68wyFaQu0rY_Y,925
114
+ tests/unit/test_type_conversion.py,sha256=zJjlBZPF4U0TJDECdFgYHPf-7lIEoQ3uy015RIRgTTA,2346
115
+ tests/unit/test_util.py,sha256=XSrZ3V2gKYhr1qEniWU9RY2Rm7PnO06ZCM05GaElncI,76
116
+ ivoryos-1.4.4.dist-info/METADATA,sha256=NEmN9NwX6lx6QAbX4JQciWSJfRNrmoFfP5qdjA6QAPY,9135
117
+ ivoryos-1.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
118
+ ivoryos-1.4.4.dist-info/top_level.txt,sha256=ZxZvj1N-GvvGOSN8pBy9SU9Ohf3ehzJfmGDh-M-0YuI,19
119
+ ivoryos-1.4.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.45.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,2 +1,3 @@
1
+ docs
1
2
  ivoryos
2
3
  tests
@@ -0,0 +1,42 @@
1
+ from unittest.mock import patch
2
+
3
+ from tests.conftest import TestEnum
4
+
5
+
6
+ def test_int_conversion(auth, test_deck):
7
+ """Tests that a string from a form is converted to an integer."""
8
+ with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.int_method') as mock_method:
9
+ auth.post('/ivoryos/control/deck.dummy/call/int_method', data={'arg': '123'})
10
+ # Check that the mock was called with an integer
11
+ mock_method.assert_called_with(arg=123)
12
+
13
+ def test_float_conversion(auth, test_deck):
14
+ """Tests that a string from a form is converted to a float."""
15
+ with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.float_method') as mock_method:
16
+ auth.post('/ivoryos/control/deck.dummy/call/float_method', data={'arg': '123.45'})
17
+ # Check that the mock was called with a float
18
+ mock_method.assert_called_with(arg=123.45)
19
+
20
+ def test_bool_conversion(auth, test_deck):
21
+ """Tests that a string from a form is converted to a boolean."""
22
+ with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.bool_method') as mock_method:
23
+ # Test with 'true'
24
+ auth.post('/ivoryos/control/deck.dummy/call/bool_method', data={'arg': 'true'})
25
+ mock_method.assert_called_with(arg=True)
26
+ # Test with 'false'
27
+ auth.post('/ivoryos/control/deck.dummy/call/bool_method', data={'arg': 'false'})
28
+ mock_method.assert_called_with(arg=False)
29
+
30
+ def test_list_conversion(auth, test_deck):
31
+ """Tests that a comma-separated string from a form is converted to a list."""
32
+ with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.list_method') as mock_method:
33
+ auth.post('/ivoryos/control/deck.dummy/call/list_method', data={'arg': 'a,b,c'})
34
+ # Check that the mock was called with a list of strings
35
+ mock_method.assert_called_with(arg=['a', 'b', 'c'])
36
+
37
+ def test_enum_conversion(auth, test_deck):
38
+ """Tests that a string from a form is converted to an Enum member."""
39
+ with patch('ivoryos.control.routes.global_config.deck_instance.deck_dummy.enum_method') as mock_method:
40
+ auth.post('/ivoryos/control/deck.dummy/call/enum_method', data={'arg': 'OPTION_B'})
41
+ # Check that the mock was called with the correct Enum member
42
+ mock_method.assert_called_with(arg=TestEnum.OPTION_B)
@@ -0,0 +1,3 @@
1
+ from ivoryos.utils import utils
2
+ from ivoryos.utils.db_models import Script
3
+
@@ -1,78 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Controller for {{instrument}}{% endblock %}
3
- {% block body %}
4
- <div id="overlay" class="overlay">
5
- <div>
6
- <h3 id="overlay-text"></h3>
7
- <div class="spinner-border" role="status"></div>
8
- </div>
9
- </div>
10
- <h1>{{instrument}} controller</h1>
11
- {% set hidden = session.get('hidden_functions', {}) %}
12
- <div class="grid-container" id="sortable-grid">
13
- {% for function, form in forms.items() %}
14
-
15
- {% set hidden_instrument = hidden.get(instrument, []) %}
16
- {% if function not in hidden_instrument %}
17
- <div class="card" id="{{function}}">
18
- <div class="bg-white rounded shadow-sm flex-fill">
19
- <i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title='{{ form.hidden_name.description or "Docstring is not available" }}' ></i>
20
- <a style="float: right" aria-label="Close" href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"><i class="bi bi-eye-slash-fill"></i></a>
21
- <div class="form-control" style="border: none">
22
- <form role="form" method='POST' name="{{function}}" id="{{function}}">
23
- <div class="form-group">
24
- {{ form.hidden_tag() }}
25
- {% for field in form %}
26
- {% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
27
- <div class="input-group mb-3">
28
- <label class="input-group-text">{{ field.label.text }}</label>
29
- {% if field.type == "SubmitField" %}
30
- {{ field(class="btn btn-dark") }}
31
- {% elif field.type == "BooleanField" %}
32
- {{ field(class="form-check-input") }}
33
- {% else %}
34
- {{ field(class="form-control") }}
35
- {% endif %}
36
- </div>
37
- {% endif %}
38
- {% endfor %}
39
- </div>
40
- <div class="input-group mb-3">
41
- <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">{{format_name(function)}} </button>
42
-
43
- </div>
44
-
45
- </form>
46
- </div>
47
- </div>
48
- </div>
49
- {% endif %}
50
- {% endfor %}
51
- </div>
52
- <div class="accordion accordion-flush" id="accordionActions" >
53
- <div class="accordion-item">
54
- <h4 class="accordion-header">
55
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#hidden">
56
- Hidden functions
57
- </button>
58
- </h4>
59
- </div>
60
- <div id="hidden" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
61
- <div class="accordion-body">
62
- {% set hidden_instrument = hidden.get(instrument, []) %}
63
- {% for function in hidden_instrument %}
64
- <div>
65
- {{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
66
- </div>
67
- {% endfor %}
68
- </div>
69
- </div>
70
- </div>
71
-
72
- <script>
73
- const saveOrderUrl = `{{ url_for('control.save_order', instrument=instrument) }}`;
74
- const buttonIds = {{ session['card_order'][instrument] | tojson }};
75
- </script>
76
- <script src="{{ url_for('static', filename='js/sortable_card.js') }}"></script>
77
- <script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
78
- {% endblock %}
@@ -1,55 +0,0 @@
1
- {% extends 'base.html' %}
2
- {% block title %}IvoryOS | Devices{% endblock %}
3
- {% block body %}
4
- <div class="row">
5
- {% if defined_variables %}
6
- {% for instrument in defined_variables %}
7
- <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
8
- <div class="bg-white rounded shadow-sm position-relative">
9
- {% if deck %}
10
- {# <a href="{{ url_for('control.disconnect', instrument=instrument) }}" class="stretched-link controller-card" style="float: right;color: red; position: relative;">Disconnect <i class="bi bi-x-square"></i></a>#}
11
- <div class="p-4 controller-card">
12
- <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument.split(".")[1]}}</a></h5>
13
- </div>
14
- {% else %}
15
- <div class="p-4 controller-card">
16
- <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a></h5>
17
- </div>
18
- {% endif %}
19
- </div>
20
- </div>
21
- {% endfor %}
22
- <div class="d-flex mb-3">
23
- <a href="{{ url_for('control.download_proxy', filetype='proxy') }}" class="btn btn-outline-primary">
24
- <i class="bi bi-download"></i> Download remote control script
25
- </a>
26
- </div>
27
- {% if not deck %}
28
- <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
29
- <div class="bg-white rounded shadow-sm position-relative">
30
- <div class="p-4 controller-card" >
31
- {% if deck %}
32
- {# todo disconnect for imported deck #}
33
- {# <h5> <a href="{{ url_for("disconnect") }}" class="stretched-link" style="color: orangered">Disconnect deck</a></h5>#}
34
- {% else %}
35
- <h5><a href="{{ url_for('control.new_controller') }}" style="color: orange" class="stretched-link">New connection</a></h5>
36
- {% endif %}
37
- </div>
38
- </div>
39
- </div>
40
- {% endif %}
41
- {% else %}
42
- <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
43
- <div class="bg-white rounded shadow-sm position-relative">
44
- <div class="p-4 controller-card" >
45
- {% if deck %}
46
- <h5><a data-bs-toggle="modal" href="#importModal" class="stretched-link"><i class="bi bi-folder-plus"></i> Import deck </a></h5>
47
- {% else %}
48
- <h5><a href="{{ url_for('control.new_controller') }}" style="color: orange" class="stretched-link">New connection</a></h5>
49
- {% endif %}
50
- </div>
51
- </div>
52
- </div>
53
- {% endif %}
54
- </div>
55
- {% endblock %}