ivoryos 0.1.21__tar.gz → 0.1.23__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 (58) hide show
  1. {ivoryos-0.1.21/ivoryos.egg-info → ivoryos-0.1.23}/PKG-INFO +7 -3
  2. {ivoryos-0.1.21 → ivoryos-0.1.23}/README.md +6 -2
  3. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/__init__.py +36 -10
  4. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/control/control.py +2 -2
  5. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/control/templates/control/controllers_home.html +6 -1
  6. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/database/database.py +59 -2
  7. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/database/templates/database/experiment_database.html +3 -3
  8. ivoryos-0.1.23/ivoryos/routes/database/templates/database/experiment_step_view.html +130 -0
  9. ivoryos-0.1.23/ivoryos/routes/database/templates/database/step_card.html +7 -0
  10. ivoryos-0.1.23/ivoryos/routes/database/templates/database/workflow_run_database.html +81 -0
  11. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/design/design.py +23 -2
  12. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/design/templates/design/experiment_builder.html +57 -10
  13. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/design/templates/design/experiment_run.html +162 -83
  14. ivoryos-0.1.23/ivoryos/routes/main/templates/main/home.html +103 -0
  15. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/js/socket_handler.js +5 -0
  16. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/templates/base.html +6 -3
  17. ivoryos-0.1.23/ivoryos/utils/client_proxy.py +57 -0
  18. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/db_models.py +43 -1
  19. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/form.py +52 -7
  20. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/llm_agent.py +1 -1
  21. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/script_runner.py +111 -44
  22. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/utils.py +23 -0
  23. ivoryos-0.1.23/ivoryos/version.py +1 -0
  24. {ivoryos-0.1.21 → ivoryos-0.1.23/ivoryos.egg-info}/PKG-INFO +7 -3
  25. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos.egg-info/SOURCES.txt +4 -0
  26. ivoryos-0.1.21/ivoryos/routes/main/templates/main/home.html +0 -70
  27. ivoryos-0.1.21/ivoryos/version.py +0 -1
  28. {ivoryos-0.1.21 → ivoryos-0.1.23}/LICENSE +0 -0
  29. {ivoryos-0.1.21 → ivoryos-0.1.23}/MANIFEST.in +0 -0
  30. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/config.py +0 -0
  31. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/__init__.py +0 -0
  32. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/auth/__init__.py +0 -0
  33. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/auth/auth.py +0 -0
  34. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/auth/templates/auth/login.html +0 -0
  35. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/auth/templates/auth/signup.html +0 -0
  36. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/control/__init__.py +0 -0
  37. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/control/templates/control/controllers.html +0 -0
  38. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/control/templates/control/controllers_new.html +0 -0
  39. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/database/__init__.py +0 -0
  40. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/design/__init__.py +0 -0
  41. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/main/__init__.py +0 -0
  42. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/main/main.py +0 -0
  43. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/routes/main/templates/main/help.html +0 -0
  44. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/favicon.ico +0 -0
  45. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/gui_annotation/Slide1.png +0 -0
  46. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/gui_annotation/Slide2.PNG +0 -0
  47. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/js/overlay.js +0 -0
  48. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/js/sortable_card.js +0 -0
  49. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/js/sortable_design.js +0 -0
  50. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/logo.webp +0 -0
  51. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/static/style.css +0 -0
  52. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/__init__.py +0 -0
  53. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos/utils/global_config.py +0 -0
  54. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos.egg-info/dependency_links.txt +0 -0
  55. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos.egg-info/requires.txt +0 -0
  56. {ivoryos-0.1.21 → ivoryos-0.1.23}/ivoryos.egg-info/top_level.txt +0 -0
  57. {ivoryos-0.1.21 → ivoryos-0.1.23}/setup.cfg +0 -0
  58. {ivoryos-0.1.21 → ivoryos-0.1.23}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ivoryos
3
- Version: 0.1.21
3
+ Version: 0.1.23
4
4
  Summary: an open-source Python package enabling Self-Driving Labs (SDLs) interoperability
5
5
  Home-page: https://gitlab.com/heingroup/ivoryos
6
6
  Author: Ivory Zhang
@@ -28,7 +28,7 @@ License-File: LICENSE
28
28
  - [Instructions for use](#instructions-for-use)
29
29
  - [Demo](#demo)
30
30
  - [Roadmap](#roadmap)
31
- - [License](#license)
31
+
32
32
 
33
33
  ## Description
34
34
  Granting SDLs flexibility and modularity makes it almost impossible to design a UI, yet it's a necessity for allowing more people to interact with it (democratisation).
@@ -85,6 +85,10 @@ Create an account and login (local database)
85
85
  - **Database**: manage workflows in _Library_ tab.
86
86
  - **Info page**: additional info in _About_ tab.
87
87
 
88
+ [//]: # (![Discord](https://img.shields.io/discord/1313641159356059770))
89
+
90
+ [//]: # (![PyPI - Downloads](https://img.shields.io/pypi/dm/ivoryos))
91
+
88
92
 
89
93
  ### Additional settings
90
94
  #### AI assistant
@@ -125,7 +129,7 @@ After one successful connection, a blueprint will be automatically saved and mad
125
129
  ivoryos.run()
126
130
  ```
127
131
  ## Demo
128
- In the [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/example/sdl_example/abstract_sdl.py), where instances of `AbstractSDL` is created as `sdl`,
132
+ In the [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/example/abstract_sdl_example/abstract_sdl.py), where instances of `AbstractSDL` is created as `sdl`,
129
133
  addresses will be available on terminal.
130
134
  ```Python
131
135
  ivoryos.run(__name__)
@@ -16,7 +16,7 @@
16
16
  - [Instructions for use](#instructions-for-use)
17
17
  - [Demo](#demo)
18
18
  - [Roadmap](#roadmap)
19
- - [License](#license)
19
+
20
20
 
21
21
  ## Description
22
22
  Granting SDLs flexibility and modularity makes it almost impossible to design a UI, yet it's a necessity for allowing more people to interact with it (democratisation).
@@ -73,6 +73,10 @@ Create an account and login (local database)
73
73
  - **Database**: manage workflows in _Library_ tab.
74
74
  - **Info page**: additional info in _About_ tab.
75
75
 
76
+ [//]: # (![Discord](https://img.shields.io/discord/1313641159356059770))
77
+
78
+ [//]: # (![PyPI - Downloads](https://img.shields.io/pypi/dm/ivoryos))
79
+
76
80
 
77
81
  ### Additional settings
78
82
  #### AI assistant
@@ -113,7 +117,7 @@ After one successful connection, a blueprint will be automatically saved and mad
113
117
  ivoryos.run()
114
118
  ```
115
119
  ## Demo
116
- In the [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/example/sdl_example/abstract_sdl.py), where instances of `AbstractSDL` is created as `sdl`,
120
+ In the [abstract_sdl.py](https://gitlab.com/heingroup/ivoryos/-/blob/main/example/abstract_sdl_example/abstract_sdl.py), where instances of `AbstractSDL` is created as `sdl`,
117
121
  addresses will be available on terminal.
118
122
  ```Python
119
123
  ivoryos.run(__name__)
@@ -2,7 +2,7 @@ import os
2
2
  import sys
3
3
  from typing import Union
4
4
 
5
- from flask import Flask, redirect, url_for, g
5
+ from flask import Flask, redirect, url_for, g, Blueprint
6
6
 
7
7
  from ivoryos.config import Config, get_config
8
8
  from ivoryos.routes.auth.auth import auth, login_manager
@@ -18,6 +18,16 @@ from ivoryos.utils.script_runner import ScriptRunner
18
18
  from ivoryos.version import __version__ as ivoryos_version
19
19
  from importlib.metadata import entry_points
20
20
  global_config = GlobalConfig()
21
+ from sqlalchemy import event
22
+ from sqlalchemy.engine import Engine
23
+ import sqlite3
24
+
25
+ @event.listens_for(Engine, "connect")
26
+ def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
27
+ if isinstance(dbapi_connection, sqlite3.Connection):
28
+ cursor = dbapi_connection.cursor()
29
+ cursor.execute("PRAGMA foreign_keys=ON")
30
+ cursor.close()
21
31
 
22
32
  url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
23
33
  app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
@@ -29,8 +39,9 @@ app.register_blueprint(database, url_prefix=url_prefix)
29
39
 
30
40
 
31
41
  def create_app(config_class=None):
32
- # url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
33
- # app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
42
+ """
43
+ create app, init database
44
+ """
34
45
  app.config.from_object(config_class or 'config.get_config()')
35
46
 
36
47
  # Initialize extensions
@@ -70,7 +81,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
70
81
  config: Config = None,
71
82
  logger: Union[str, list] = None,
72
83
  logger_output_name: str = None,
73
- enable_design=True
84
+ enable_design=True,
85
+ blueprint_plugins=None,
74
86
  ):
75
87
  """
76
88
  Start ivoryOS app server.
@@ -85,11 +97,14 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
85
97
  :param logger: logger name of list of logger names, defaults to None
86
98
  :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
87
99
  :param enable_design: enable design canvas, database and workflow execution
88
- :param stream_address:
89
100
  """
90
101
  app = create_app(config_class=config or get_config()) # Create app instance using factory function
91
102
 
92
- plugins = load_plugins(app, socketio)
103
+ plugins = load_installed_plugins(app, socketio)
104
+
105
+ if blueprint_plugins:
106
+ config_plugins = load_plugins(blueprint_plugins, app, socketio)
107
+ plugins.extend(config_plugins)
93
108
 
94
109
  def inject_nav_config():
95
110
  """Make NAV_CONFIG available globally to all templates."""
@@ -110,10 +125,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
110
125
  app.config["MODULE"] = module
111
126
  app.config["OFF_LINE"] = False
112
127
  global_config.deck = sys.modules[module]
113
- # global_config.heinsight = HeinsightAPI("http://127.0.0.1:8080")
114
128
  global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
115
129
  output_path=app.config["DUMMY_DECK"], save=True)
116
- # global_config.runner = ScriptRunner(globals())
117
130
  else:
118
131
  app.config["OFF_LINE"] = True
119
132
  if model:
@@ -135,7 +148,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
135
148
  # return app
136
149
 
137
150
 
138
- def load_plugins(app, socketio):
151
+ def load_installed_plugins(app, socketio):
139
152
  """
140
153
  Dynamically load installed plugins and attach Flask-SocketIO.
141
154
  """
@@ -152,4 +165,17 @@ def load_plugins(app, socketio):
152
165
 
153
166
  return plugin_names
154
167
 
155
-
168
+ def load_plugins(blueprints:list[Blueprint], app, socketio):
169
+ """
170
+ Dynamically load installed plugins and attach Flask-SocketIO.
171
+ """
172
+ plugin_names = []
173
+ if not isinstance(blueprints, list):
174
+ blueprints = [blueprints]
175
+ for blueprint in blueprints:
176
+ # If the plugin has an `init_socketio()` function, pass socketio
177
+ if hasattr(blueprint, 'init_socketio'):
178
+ blueprint.init_socketio(socketio)
179
+ plugin_names.append(blueprint.name)
180
+ app.register_blueprint(blueprint, url_prefix=f"{url_prefix}/{blueprint.name}")
181
+ return plugin_names
@@ -151,7 +151,7 @@ def controllers(instrument: str):
151
151
  return render_template('controllers.html', instrument=instrument, forms=forms, format_name=format_name)
152
152
 
153
153
 
154
- @control.route("/backend_control/<instrument>", methods=['GET', 'POST'])
154
+ @control.route("/backend_control/<instrument>", methods=['POST'])
155
155
  def backend_control(instrument: str=None):
156
156
  """
157
157
  .. :quickref: Backend Control; backend control
@@ -187,7 +187,7 @@ def backend_control(instrument: str=None):
187
187
  return json_output, 400
188
188
  else:
189
189
  return "instrument not exist", 400
190
- return json_output, 200
190
+ return json_output, 200
191
191
 
192
192
 
193
193
  @control.route("/backend_control", methods=['GET'])
@@ -9,7 +9,7 @@
9
9
  {# {% if not deck %}#}
10
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
11
  <div class="p-4 controller-card">
12
- <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a></h5>
12
+ <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument.split(".")[1]}}</a></h5>
13
13
  </div>
14
14
  {# {% else %}#}
15
15
  {# <div class="p-4 controller-card">#}
@@ -19,6 +19,11 @@
19
19
  </div>
20
20
  </div>
21
21
  {% endfor %}
22
+ <div class="d-flex mb-3">
23
+ <a href="{{ url_for('design.download', filetype='proxy') }}" class="btn btn-outline-primary">
24
+ <i class="bi bi-download"></i> Download remote control script
25
+ </a>
26
+ </div>
22
27
  {% if not deck %}
23
28
  <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
24
29
  <div class="bg-white rounded shadow-sm position-relative">
@@ -1,7 +1,7 @@
1
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app
1
+ from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
2
2
  from flask_login import login_required
3
3
 
4
- from ivoryos.utils.db_models import Script, db
4
+ from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
5
5
  from ivoryos.utils.utils import get_script_file, post_script_file
6
6
 
7
7
  database = Blueprint('database', __name__, template_folder='templates/database')
@@ -188,3 +188,60 @@ def save_as():
188
188
  else:
189
189
  flash("Script name is already exist in database")
190
190
  return redirect(url_for("design.experiment_builder"))
191
+
192
+
193
+ @database.route('/workflow_runs')
194
+ def list_workflows():
195
+ query = WorkflowRun.query
196
+ search_term = request.args.get("keyword", None)
197
+ if search_term:
198
+ query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
199
+ page = request.args.get('page', default=1, type=int)
200
+ per_page = 10
201
+
202
+ workflows = query.paginate(page=page, per_page=per_page, error_out=False)
203
+ return render_template('workflow_run_database.html', workflows=workflows)
204
+
205
+
206
+ @database.route("/workflow_steps/<int:workflow_id>")
207
+ def get_workflow_steps(workflow_id):
208
+ workflow = WorkflowRun.query.get_or_404(workflow_id)
209
+ steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
210
+
211
+ # Organize steps by phase + repeat_index
212
+ grouped = {
213
+ "prep": [],
214
+ "script": {},
215
+ "cleanup": [],
216
+ }
217
+
218
+ for step in steps:
219
+ if step.phase == "prep":
220
+ grouped["prep"].append(step)
221
+ elif step.phase == "script":
222
+ grouped["script"].setdefault(step.repeat_index, []).append(step)
223
+ elif step.phase == "cleanup" or step.method_name == "stop":
224
+ grouped["cleanup"].append(step)
225
+
226
+ return render_template("experiment_step_view.html", workflow=workflow, grouped=grouped)
227
+
228
+
229
+ @database.route("/delete_workflow_data/<workflow_id>")
230
+ @login_required
231
+ def delete_workflow_data(workflow_id: str):
232
+ """
233
+ .. :quickref: Database; delete experiment data from database
234
+
235
+ delete workflow data from database
236
+
237
+ .. http:get:: /delete_workflow_data/<workflow_id>
238
+
239
+ :param workflow_id: workflow id
240
+ :type workflow_id: str
241
+ :status 302: redirect to :http:get:`/ivoryos/workflow_runs/`
242
+
243
+ """
244
+ run = WorkflowRun.query.get(workflow_id)
245
+ db.session.delete(run)
246
+ db.session.commit()
247
+ return redirect(url_for('database.list_workflows'))
@@ -30,7 +30,7 @@
30
30
  <th scope="col">Time created</th>
31
31
  <th scope="col">Last modified</th>
32
32
  <th scope="col">Author</th>
33
- <th scope="col">Registered</th>
33
+ {# <th scope="col">Registered</th>#}
34
34
  <th scope="col"></th>
35
35
  </tr>
36
36
  </thead>
@@ -43,13 +43,13 @@
43
43
  <td>{{ workflow.time_created }}</td>
44
44
  <td>{{ workflow.last_modified }}</td>
45
45
  <td>{{ workflow.author }}</td>
46
- <td>{{ workflow.registered }}</td>
46
+ {# <td>{{ workflow.registered }}</td>#}
47
47
  <td>
48
48
  {#not workflow.status == "finalized" or#}
49
49
  {% if session['user'] == 'admin' or session['user'] == workflow.author %}
50
50
  <a href="{{ url_for('database.delete_workflow', workflow_name=workflow.name) }}">delete</a>
51
51
  {% else %}
52
- <a class="disabled-link" href="{{ url_for('database.delete_workflow', workflow_name=workflow.name) }}">delete</a>
52
+ <a class="disabled-link">delete</a>
53
53
  {% endif %}
54
54
  <td>
55
55
  </tr>
@@ -0,0 +1,130 @@
1
+ {% extends 'base.html' %}
2
+
3
+ {% block title %}IvoryOS | Experiment Results{% endblock %}
4
+
5
+ {% block body %}
6
+ <style>
7
+ .vis-time-axis .vis-text.vis-minor,
8
+ .vis-time-axis .vis-text.vis-major {
9
+ color: #666;
10
+ }
11
+ .vis-item.stop {
12
+ background-color: red;
13
+ color: white;
14
+ border: none;
15
+ font-weight: bold;
16
+ }
17
+ </style>
18
+
19
+ <div id="timeline"></div>
20
+
21
+ <script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script>
22
+ <link href="https://unpkg.com/vis-timeline@latest/styles/vis-timeline-graph2d.min.css" rel="stylesheet"/>
23
+
24
+ <h1>Experiment Step View</h1>
25
+
26
+ <div id="visualization"></div>
27
+
28
+ <script type="text/javascript">
29
+ var container = document.getElementById('visualization');
30
+
31
+ const items = [
32
+ {% if grouped.prep %}
33
+ {
34
+ id: 'prep',
35
+ content: 'Prep Phase',
36
+ start: '{{ grouped.prep[0].start_time }}',
37
+ end: '{{ grouped.prep[-1].end_time }}',
38
+ className: 'prep',
39
+ group: 'prep'
40
+ },
41
+ {% endif %}
42
+
43
+ {% for repeat_index, step_list in grouped.script.items()|sort %}
44
+ {
45
+ id: 'iter{{ repeat_index }}',
46
+ content: 'Iteration {{ repeat_index }}',
47
+ start: '{{ step_list[0].start_time }}',
48
+ end: '{{ step_list[-1].end_time }}',
49
+ className: 'script',
50
+ group: 'iter{{ repeat_index }}'
51
+ },
52
+ {% for step in step_list %}
53
+ {% if step.method_name == "stop" %}
54
+ {
55
+ id: 'stop-{{ step.id }}',
56
+ content: '🛑 Stop',
57
+ start: '{{ step.start_time }}',
58
+ type: 'point',
59
+ className: 'stop',
60
+ group: 'iter{{ repeat_index }}'
61
+ },
62
+ {% endif %}
63
+ {% endfor %}
64
+ {% endfor %}
65
+
66
+ {% if grouped.cleanup %}
67
+ {
68
+ id: 'cleanup',
69
+ content: 'Cleanup Phase',
70
+ start: '{{ grouped.cleanup[0].start_time }}',
71
+ end: '{{ grouped.cleanup[-1].end_time }}',
72
+ className: 'cleanup',
73
+ group: 'cleanup'
74
+
75
+ },
76
+ {% endif %}
77
+ ];
78
+
79
+ const groups = [
80
+ {% if grouped.prep %}{ id: 'prep', content: 'Prep' },{% endif %}
81
+ {% for repeat_index in grouped.script.keys()|sort %}{ id: 'iter{{ repeat_index }}', content: 'Iteration {{ repeat_index }}' },{% endfor %}
82
+ {% if grouped.cleanup %}{ id: 'cleanup', content: 'Cleanup' },{% endif %}
83
+ ];
84
+
85
+ var options = {
86
+ clickToUse: true,
87
+ stack: false, // important to keep point within group row
88
+ horizontalScroll: true,
89
+ zoomKey: 'ctrlKey'
90
+ };
91
+
92
+ // Initialize your timeline with the sorted groups
93
+ const timeline = new vis.Timeline(container, items, groups, options);
94
+
95
+ timeline.on('select', function (props) {
96
+ const id = props.items[0];
97
+ if (id && id.startsWith('iter')) {
98
+ const card = document.getElementById('card-' + id);
99
+ if (card) {
100
+ const yOffset = -80;
101
+ const y = card.getBoundingClientRect().top + window.pageYOffset + yOffset;
102
+ window.scrollTo({ top: y, behavior: 'smooth' });
103
+ }
104
+ }
105
+ });
106
+ </script>
107
+
108
+ <h2>Workflow: {{ workflow.name }}</h2>
109
+
110
+ {% if grouped.prep %}
111
+ <h4 class="mt-4">Prep Phase</h4>
112
+ {% for step in grouped.prep %}
113
+ {% include "step_card.html" %}
114
+ {% endfor %}
115
+ {% endif %}
116
+
117
+ {% for repeat_index, step_list in grouped.script.items()|sort %}
118
+ <h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
119
+ {% for step in step_list %}
120
+ {% include "step_card.html" %}
121
+ {% endfor %}
122
+ {% endfor %}
123
+
124
+ {% if grouped.cleanup %}
125
+ <h4 class="mt-4">Cleanup Phase</h4>
126
+ {% for step in grouped.cleanup %}
127
+ {% include "step_card.html" %}
128
+ {% endfor %}
129
+ {% endif %}
130
+ {% endblock %}
@@ -0,0 +1,7 @@
1
+ <div class="card mb-2 {{ 'border-danger text-danger bg-light' if step.run_error else 'border-secondary' }}">
2
+ <div class="card-body p-2">
3
+ <strong>{{ step.method_name }}</strong>
4
+ <small>Start: {{ step.start_time }}</small>
5
+ <small>End: {{ step.end_time }}</small>
6
+ </div>
7
+ </div>
@@ -0,0 +1,81 @@
1
+ {% extends 'base.html' %}
2
+
3
+ {% block title %}IvoryOS | Design Database{% endblock %}
4
+ {% block body %}
5
+
6
+ <table class="table table-hover" id="workflowResultLibrary">
7
+ <thead>
8
+ <tr>
9
+ <th scope="col">Workflow name</th>
10
+ <th scope="col">Start time</th>
11
+ <th scope="col">End time</th>
12
+ <th scope="col">Data</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ {% for workflow in workflows %}
17
+ <tr>
18
+ <td><a href="{{ url_for('database.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
19
+ <td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
20
+ <td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
21
+
22
+ <td>
23
+ {% if workflow.data_path %}
24
+ <a href="{{ url_for('design.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
25
+ {% endif %}
26
+ </td>
27
+ <td>
28
+ {% if session['user'] == 'admin' or session['user'] == workflow.author %}
29
+ <a href="{{ url_for('database.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
30
+ {% else %}
31
+ <a class="disabled-link">delete</a>
32
+ {% endif %}
33
+ </td>
34
+ </tr>
35
+ {% endfor %}
36
+ </tbody>
37
+ </table>
38
+
39
+ {# paging#}
40
+ <div class="pagination justify-content-center">
41
+ <div class="page-item {{ 'disabled' if not workflows.has_prev else '' }}">
42
+ <a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.prev_num) }}">Previous</a>
43
+ </div>
44
+ {% for num in workflows.iter_pages() %}
45
+ <div class="page-item">
46
+ <a class="page-link {{ 'active' if num == workflows.page else '' }}" href="{{ url_for('database.list_workflows', page=num) }}">{{ num }}</a>
47
+ </div>
48
+ {% endfor %}
49
+ <div class="page-item {{ 'disabled' if not workflows.has_next else '' }}">
50
+ <a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.next_num) }}">Next</a>
51
+ </div>
52
+ </div>
53
+
54
+ <div id="steps-container"></div>
55
+
56
+ <script>
57
+ function showSteps(workflowId) {
58
+ fetch(`/workflow_steps/${workflowId}`)
59
+ .then(response => response.json())
60
+ .then(data => {
61
+ const container = document.getElementById('steps-container');
62
+ container.innerHTML = ''; // Clear previous content
63
+ const stepsList = document.createElement('ul');
64
+
65
+ data.steps.forEach(step => {
66
+ const li = document.createElement('li');
67
+ li.innerHTML = `
68
+ <strong>Step: </strong> ${step.method_name} <br>
69
+ <strong>Start Time:</strong> ${step.start_time} <br>
70
+ <strong>End Time:</strong> ${step.end_time} <br>
71
+ <strong>Human Intervention:</strong> ${step.run_error ? 'Yes' : 'No'}
72
+ `;
73
+ stepsList.appendChild(li);
74
+ });
75
+
76
+ container.appendChild(stepsList);
77
+ });
78
+ }
79
+ </script>
80
+
81
+ {% endblock %}
@@ -11,6 +11,7 @@ from flask_socketio import SocketIO
11
11
  from werkzeug.utils import secure_filename
12
12
 
13
13
  from ivoryos.utils import utils
14
+ from ivoryos.utils.client_proxy import create_function, export_to_python
14
15
  from ivoryos.utils.global_config import GlobalConfig
15
16
  from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
16
17
  create_form_from_action, create_all_builtin_forms
@@ -39,6 +40,13 @@ def handle_abort_current():
39
40
 
40
41
  @socketio.on('pause')
41
42
  def handle_pause():
43
+ runner.retry = False
44
+ msg = runner.toggle_pause()
45
+ socketio.emit('log', {'message': msg})
46
+
47
+ @socketio.on('retry')
48
+ def handle_pause():
49
+ runner.retry = True
42
50
  msg = runner.toggle_pause()
43
51
  socketio.emit('log', {'message': msg})
44
52
 
@@ -360,7 +368,7 @@ def experiment_run():
360
368
  run_name = script.validate_function_name(run_name)
361
369
  runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
362
370
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
363
- output_path=datapath
371
+ output_path=datapath, current_app=current_app._get_current_object()
364
372
  )
365
373
  if utils.check_config_duplicate(config):
366
374
  flash(f"WARNING: Duplicate in config entries.")
@@ -525,7 +533,20 @@ def download(filetype):
525
533
  outfile.write(json_object)
526
534
  elif filetype == "python":
527
535
  filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
528
-
536
+ elif filetype == "proxy":
537
+ snapshot = global_config.deck_snapshot.copy()
538
+ class_definitions = {}
539
+ # Iterate through each instrument in the snapshot
540
+ for instrument_key, instrument_data in snapshot.items():
541
+ # Iterate through each function associated with the current instrument
542
+ for function_key, function_data in instrument_data.items():
543
+ # Convert the function signature to a string representation
544
+ function_data['signature'] = str(function_data['signature'])
545
+ class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
546
+ class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
547
+ # Export the generated class definitions to a .py script
548
+ export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
549
+ filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
529
550
  return send_file(os.path.abspath(filepath), as_attachment=True)
530
551
 
531
552