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
@@ -1,786 +1,477 @@
1
- import csv
2
- import json
3
1
  import os
4
- import pickle
5
- import sys
6
- import time
7
2
 
8
- from flask import Blueprint, redirect, url_for, flash, jsonify, send_file, request, render_template, session, \
9
- current_app, g
10
- from flask_login import login_required
11
- from flask_socketio import SocketIO
12
- from werkzeug.utils import secure_filename
3
+ from flask import Blueprint, redirect, url_for, flash, jsonify, request, render_template, session, current_app
4
+ from flask_login import login_required, current_user
13
5
 
6
+ from ivoryos.routes.library.library import publish
14
7
  from ivoryos.utils import utils
15
8
  from ivoryos.utils.global_config import GlobalConfig
16
- from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
17
- create_form_from_action, create_all_builtin_forms
18
- from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
19
- from ivoryos.utils.script_runner import ScriptRunner
20
- # from ivoryos.utils.utils import load_workflows
9
+ from ivoryos.utils.form import create_action_button, create_form_from_pseudo, create_all_builtin_forms
10
+ from ivoryos.utils.db_models import Script
11
+ from ivoryos.utils.py_to_json import convert_to_cards
21
12
 
22
- socketio = SocketIO()
23
- design = Blueprint('design', __name__, template_folder='templates/design')
13
+ # Import the new modular components
14
+ from ivoryos.routes.design.design_file import files
15
+ from ivoryos.routes.design.design_step import steps
24
16
 
25
- global_config = GlobalConfig()
26
- runner = ScriptRunner()
27
-
28
- def abort_pending():
29
- runner.abort_pending()
30
- socketio.emit('log', {'message': "aborted pending iterations"})
31
-
32
- def abort_current():
33
- runner.stop_execution()
34
- socketio.emit('log', {'message': "stopped next task"})
35
-
36
- def pause():
37
- runner.retry = False
38
- msg = runner.toggle_pause()
39
- socketio.emit('log', {'message': msg})
40
- return msg
41
-
42
- def retry():
43
- runner.retry = True
44
- msg = runner.toggle_pause()
45
- socketio.emit('log', {'message': msg})
46
-
47
-
48
- # ---- Socket.IO Event Handlers ----
49
-
50
- @socketio.on('abort_pending')
51
- def handle_abort_pending():
52
- abort_pending()
53
17
 
54
- @socketio.on('abort_current')
55
- def handle_abort_current():
56
- abort_current()
18
+ design = Blueprint('design', __name__, template_folder='templates')
57
19
 
58
- @socketio.on('pause')
59
- def handle_pause():
60
- pause()
61
-
62
- @socketio.on('retry')
63
- def handle_retry():
64
- retry()
20
+ # Register sub-blueprints
21
+ design.register_blueprint(files)
22
+ design.register_blueprint(steps)
65
23
 
24
+ global_config = GlobalConfig()
66
25
 
67
- @socketio.on('connect')
68
- def handle_abort_action():
69
- # Fetch log messages from local file
70
- filename = os.path.join(current_app.config["OUTPUT_FOLDER"], current_app.config["LOGGERS_PATH"])
71
- with open(filename, 'r') as log_file:
72
- log_history = log_file.readlines()
73
- for message in log_history[-10:]:
74
- socketio.emit('log', {'message': message})
26
+ # ---- Main Design Routes ----
75
27
 
76
28
 
77
- @design.route("/design/script/", methods=['GET', 'POST'])
78
- @design.route("/design/script/<instrument>/", methods=['GET', 'POST'])
29
+ def _create_forms(instrument, script, autofill, pseudo_deck = None):
30
+ deck = global_config.deck
31
+ functions = {}
32
+ if instrument == 'flow_control':
33
+ forms = create_all_builtin_forms(script=script)
34
+ elif instrument in global_config.defined_variables.keys():
35
+ _object = global_config.defined_variables.get(instrument)
36
+ functions = utils._inspect_class(_object)
37
+ forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
38
+ elif instrument.startswith("blocks"):
39
+ forms = create_form_from_pseudo(pseudo=global_config.building_blocks[instrument], autofill=autofill, script=script)
40
+ functions = global_config.building_blocks[instrument]
41
+ else:
42
+ if deck:
43
+ functions = global_config.deck_snapshot.get(instrument, {})
44
+ elif pseudo_deck:
45
+ functions = pseudo_deck.get(instrument, {})
46
+ forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
47
+ return functions, forms
48
+
49
+ @design.route("/draft")
79
50
  @login_required
80
- def experiment_builder(instrument=None):
51
+ def experiment_builder():
81
52
  """
82
53
  .. :quickref: Workflow Design; Build experiment workflow
83
54
 
84
55
  **Experiment Builder**
85
56
 
86
- This route allows users to build and edit experiment workflows. Users can interact with available instruments,
87
- define variables, and manage experiment scripts.
88
-
89
- .. http:get:: /design/script
57
+ .. http:get:: /draft
90
58
 
91
- Load the experiment builder interface.
59
+ Load the experiment builder page where users can design their workflow by adding actions, instruments, and logic.
92
60
 
93
- :param instrument: The specific instrument for which to load functions and forms.
94
- :type instrument: str
95
61
  :status 200: Experiment builder loaded successfully.
96
62
 
97
- .. http:post:: /design/script
98
-
99
- Submit form data to add or modify actions in the experiment script.
100
-
101
- **Adding action to canvas**
102
-
103
- :form return: (optional) The name of the function or method to add to the script.
104
- :form dynamic: depend on the selected instrument and its metadata.
105
-
106
- :status 200: Action added or modified successfully.
107
- :status 400: Validation errors in submitted form data.
108
- :status 302: Toggles autofill or redirects to refresh the page.
109
-
110
- **Toggle auto parameter name fill**:
111
-
112
- :status 200: autofill toggled successfully
113
-
114
63
  """
115
64
  deck = global_config.deck
116
65
  script = utils.get_script_file()
117
- # load_workflows(script)
118
- # registered_workflows = global_config.registered_workflows
119
66
 
120
67
  if deck and script.deck is None:
121
68
  script.deck = os.path.splitext(os.path.basename(deck.__file__))[
122
69
  0] if deck.__name__ == "__main__" else deck.__name__
123
- # script.sort_actions()
124
-
70
+ utils.post_script_file(script)
125
71
  pseudo_deck_name = session.get('pseudo_deck', '')
126
72
  pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
127
73
  off_line = current_app.config["OFF_LINE"]
128
- enable_llm = current_app.config["ENABLE_LLM"]
129
- autofill = session.get('autofill')
130
74
 
131
- # autofill is not allowed for prep and cleanup
132
- autofill = autofill if script.editing_type == "script" else False
133
- forms = None
134
75
  pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
135
76
  if off_line and pseudo_deck is None:
136
77
  flash("Choose available deck below.")
137
78
 
138
79
  deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
139
80
 
140
- functions = {}
141
81
  if deck:
142
82
  deck_variables = list(global_config.deck_snapshot.keys())
143
- # deck_variables.insert(0, "registered_workflows")
144
- deck_variables.insert(0, "flow_control")
145
-
83
+ # deck_variables.insert(0, "flow_control")
146
84
  else:
147
85
  deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
148
86
  deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
149
- edit_action_info = session.get("edit_action")
150
- if edit_action_info:
151
- forms = create_form_from_action(edit_action_info, script=script)
152
- elif instrument:
153
- # if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
154
- # forms = create_builtin_form(instrument, script=script)
155
- if instrument == 'flow_control':
156
- forms = create_all_builtin_forms(script=script)
157
- # elif instrument == 'registered_workflows':
158
- # functions = utils._inspect_class(registered_workflows)
159
- # # forms = create_workflow_forms(script=script)
160
- # forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
161
- elif instrument in global_config.defined_variables.keys():
162
- _object = global_config.defined_variables.get(instrument)
163
- functions = utils._inspect_class(_object)
164
- forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
165
- else:
166
- if deck:
167
- functions = global_config.deck_snapshot.get(instrument, {})
168
- elif pseudo_deck:
169
- functions = pseudo_deck.get(instrument, {})
170
- forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
171
- if request.method == 'POST' and "hidden_name" in request.form:
172
- # all_kwargs = request.form.copy()
173
- method_name = request.form.get("hidden_name", None)
174
- # if method_name is not None:
175
- form = forms.get(method_name)
176
- insert_position = request.form.get("drop_target_id", None)
177
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
178
- if form and form.validate_on_submit():
179
- function_name = kwargs.pop("hidden_name")
180
- save_data = kwargs.pop('return', '')
181
-
182
- primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
183
-
184
- script.eval_list(kwargs, primitive_arg_types)
185
- kwargs = script.validate_variables(kwargs)
186
- action = {"instrument": instrument, "action": function_name,
187
- "args": kwargs,
188
- "return": save_data,
189
- 'arg_types': primitive_arg_types}
190
- script.add_action(action=action, insert_position=insert_position)
191
- else:
192
- flash(form.errors)
193
-
194
- elif request.method == 'POST' and "builtin_name" in request.form:
195
- function_name = request.form.get("builtin_name")
196
- form = forms.get(function_name)
197
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
198
- insert_position = request.form.get("drop_target_id", None)
199
-
200
- if form.validate_on_submit():
201
- # print(kwargs)
202
- logic_type = kwargs.pop('builtin_name')
203
- if 'variable' in kwargs:
204
- try:
205
- script.add_variable(insert_position=insert_position, **kwargs)
206
- except ValueError:
207
- flash("Invalid variable type")
208
- else:
209
- script.add_logic_action(logic_type=logic_type, insert_position=insert_position, **kwargs)
210
- else:
211
- flash(form.errors)
212
- elif request.method == 'POST' and "workflow_name" in request.form:
213
- workflow_name = request.form.get("workflow_name")
214
- form = forms.get(workflow_name)
215
- kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
216
- insert_position = request.form.get("drop_target_id", None)
217
-
218
- if form.validate_on_submit():
219
- # workflow_name = kwargs.pop('workflow_name')
220
- save_data = kwargs.pop('return', '')
221
-
222
- primitive_arg_types = utils.get_arg_type(kwargs, functions[workflow_name])
223
-
224
- script.eval_list(kwargs, primitive_arg_types)
225
- kwargs = script.validate_variables(kwargs)
226
- action = {"instrument": instrument, "action": workflow_name,
227
- "args": kwargs,
228
- "return": save_data,
229
- 'arg_types': primitive_arg_types}
230
- script.add_action(action=action, insert_position=insert_position)
231
- script.add_workflow(**kwargs, insert_position=insert_position)
232
- else:
233
- flash(form.errors)
234
-
235
- # toggle autofill, autofill doesn't apply to control flow ops
236
- elif request.method == 'POST' and "autofill" in request.form:
237
- autofill = not autofill
238
- session['autofill'] = autofill
239
- if not instrument == 'flow_control':
240
- forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
241
87
 
242
- utils.post_script_file(script)
88
+ # edit_action_info = session.get("edit_action")
243
89
 
244
- 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}")
245
95
  session['python_code'] = exec_string
246
96
 
247
- design_buttons = create_action_button(script)
248
- return render_template('experiment_builder.html', off_line=off_line, instrument=instrument, history=deck_list,
249
- script=script, defined_variables=deck_variables,
250
- local_variables=global_config.defined_variables,
251
- forms=forms, buttons=design_buttons, format_name=format_name,
252
- use_llm=enable_llm)
97
+ design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
253
98
 
99
+ return render_template('experiment_builder.html', off_line=off_line, history=deck_list,
100
+ script=script, defined_variables=deck_variables, buttons_dict=design_buttons,
101
+ local_variables=global_config.defined_variables, block_variables=global_config.building_blocks)
254
102
 
255
- @design.route("/design/generate_code", methods=['POST'])
103
+ @design.route("/draft/code_preview", methods=["GET"])
256
104
  @login_required
257
- def generate_code():
258
- """
259
- .. :quickref: Text to Code; Generate code from user input and update the design canvas.
105
+ def compile_preview():
106
+ # Get mode and batch from query parameters
107
+ script = utils.get_script_file()
108
+ mode = request.args.get("mode", "single") # default to "single"
109
+ batch = request.args.get("batch", "sample") # default to "sample"
260
110
 
261
- .. http:post:: /design/generate_code
111
+ try:
112
+ # Example: decide which code to return based on mode/batch
113
+ if mode == "single":
114
+ code = script.compile(current_app.config['SCRIPT_FOLDER'])
115
+ elif mode == "batch":
116
+ code = script.compile(current_app.config['SCRIPT_FOLDER'], batch=True, mode=batch)
117
+ else:
118
+ code = "Invalid mode. Please select 'single' or 'batch'."
119
+ except Exception as e:
120
+ code = f"Error compiling: {e}"
121
+ # print(code)
122
+ return jsonify(code=code)
262
123
 
263
- :form prompt: user's prompt
264
- :status 200: and then redirects to :http:get:`/experiment/build`
265
- :status 400: failed to initialize the AI agent redirects to :http:get:`/design/script`
266
124
 
267
- """
268
- agent = global_config.agent
269
- enable_llm = current_app.config["ENABLE_LLM"]
270
- instrument = request.form.get("instrument")
271
-
272
- if request.method == 'POST' and "clear" in request.form:
273
- session['prompt'][instrument] = ''
274
- if request.method == 'POST' and "gen" in request.form:
275
- prompt = request.form.get("prompt")
276
- session['prompt'][instrument] = prompt
277
- # sdl_module = utils.parse_functions(find_instrument_by_name(f'deck.{instrument}'), doc_string=True)
278
- sdl_module = global_config.deck_snapshot.get(instrument, {})
279
- empty_script = Script(author=session.get('user'))
280
- if enable_llm and agent is None:
281
- try:
282
- model = current_app.config["LLM_MODEL"]
283
- server = current_app.config["LLM_SERVER"]
284
- module = current_app.config["MODULE"]
285
- from ivoryos.utils.llm_agent import LlmAgent
286
- agent = LlmAgent(host=server, model=model, output_path=os.path.dirname(os.path.abspath(module)))
287
- except Exception as e:
288
- flash(e.__str__())
289
- return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True)), 400
290
- action_list = agent.generate_code(sdl_module, prompt)
291
- for action in action_list:
292
- action['instrument'] = instrument
293
- action['return'] = ''
294
- if "args" not in action:
295
- action['args'] = {}
296
- if "arg_types" not in action:
297
- action['arg_types'] = {}
298
- empty_script.add_action(action)
299
- utils.post_script_file(empty_script)
300
- return redirect(url_for("design.experiment_builder", instrument=instrument, use_llm=True))
301
-
302
-
303
- @design.route("/design/campaign", methods=['GET', 'POST'])
125
+ @design.route("/draft/meta", methods=["PATCH"])
304
126
  @login_required
305
- def experiment_run():
127
+ def update_script_meta():
306
128
  """
307
- .. :quickref: Workflow Execution; Execute/iterate the workflow
308
-
309
- .. http:get:: /design/campaign
129
+ .. :quickref: Workflow Design; update the script metadata.
310
130
 
311
- Compile the workflow and load the experiment execution interface.
131
+ .. http:patch:: /draft/meta
312
132
 
313
- .. http:post:: /design/campaign
133
+ Update the script metadata, including the script name and status. If the script name is provided,
134
+ it saves the script with that name. If the status is "finished", it finalizes the script.
314
135
 
315
- Start workflow execution
136
+ :form name: The name to save the script as.
137
+ :form status: The status of the script (e.g., "finished").
316
138
 
139
+ :status 200: Successfully updated the script metadata.
317
140
  """
318
- deck = global_config.deck
319
- script = utils.get_script_file()
320
-
321
- # script.sort_actions() # handled in update list
322
- off_line = current_app.config["OFF_LINE"]
323
- deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
324
- # if not off_line and deck is None:
325
- # # print("loading deck")
326
- # module = current_app.config.get('MODULE', '')
327
- # deck = sys.modules[module] if module else None
328
- # script.deck = os.path.splitext(os.path.basename(deck.__file__))[0]
329
- design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
330
- config_preview = []
331
- config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
332
- try:
333
- # todo
334
- exec_string = script.python_script if script.python_script else script.compile(current_app.config['SCRIPT_FOLDER'])
335
- # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
336
- # print(exec_string)
337
- except Exception as e:
338
- flash(e.__str__())
339
- # handle api request
340
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
341
- return jsonify({"error": e.__str__()})
342
- else:
343
- return redirect(url_for("design.experiment_builder"))
344
-
345
- config_file = request.args.get("filename")
346
- config = []
347
- if config_file:
348
- session['config_file'] = config_file
349
- filename = session.get("config_file")
350
- if filename:
351
- # config_preview = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
352
- config = list(csv.DictReader(open(os.path.join(current_app.config['CSV_FOLDER'], filename))))
353
- config_preview = config[1:]
354
- arg_type = config.pop(0) # first entry is types
355
- try:
356
- for key, func_str in exec_string.items():
357
- exec(func_str)
358
- line_collection = script.convert_to_lines(exec_string)
359
-
360
- except Exception:
361
- flash(f"Please check {key} syntax!!")
362
- return redirect(url_for("design.experiment_builder"))
363
- # runner.globals_dict.update(globals())
364
- run_name = script.name if script.name else "untitled"
365
-
366
- dismiss = session.get("dismiss", None)
141
+ data = request.get_json()
367
142
  script = utils.get_script_file()
368
- no_deck_warning = False
369
-
370
- _, return_list = script.config_return()
371
- config_list, config_type_list = script.config("script")
372
- # config = script.config("script")
373
- data_list = os.listdir(current_app.config['DATA_FOLDER'])
374
- data_list.remove(".gitkeep") if ".gitkeep" in data_list else data_list
375
- if deck is None:
376
- no_deck_warning = True
377
- flash(f"No deck is found, import {script.deck}")
378
- elif script.deck:
379
- is_deck_match = script.deck == deck.__name__ or script.deck == \
380
- os.path.splitext(os.path.basename(deck.__file__))[0]
381
- if not is_deck_match:
382
- flash(f"This script is not compatible with current deck, import {script.deck}")
383
- if request.method == "POST":
384
- bo_args = None
385
- compiled = False
386
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
387
- payload_json = request.get_json()
388
- compiled = True
389
- if "kwargs" in payload_json:
390
- config = payload_json["kwargs"]
391
- elif "parameters" in payload_json:
392
- bo_args = payload_json
393
- repeat = payload_json.pop("repeat", None)
143
+ if 'name' in data:
144
+ run_name = data.get("name")
145
+ exist_script = Script.query.get(run_name)
146
+ if exist_script is None:
147
+ script.save_as(run_name)
148
+ utils.post_script_file(script)
149
+ return jsonify(success=True)
394
150
  else:
395
- if "bo" in request.form:
396
- bo_args = request.form.to_dict()
397
- if "online-config" in request.form:
398
- config = utils.web_config_entry_wrapper(request.form.to_dict(), config_list)
399
- repeat = request.form.get('repeat', None)
151
+ flash("Script name is already exist in database")
152
+ return jsonify(success=False)
400
153
 
401
- try:
402
- datapath = current_app.config["DATA_FOLDER"]
403
- run_name = script.validate_function_name(run_name)
404
- runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
405
- logger=g.logger, socketio=g.socketio, repeat_count=repeat,
406
- output_path=datapath, compiled=compiled,
407
- current_app=current_app._get_current_object()
408
- )
409
- if utils.check_config_duplicate(config):
410
- flash(f"WARNING: Duplicate in config entries.")
411
- except Exception as e:
412
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
413
- return jsonify({"error": e.__str__()})
414
- else:
415
- flash(e)
416
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
417
- # wait to get a workflow ID
418
- while not global_config.runner_status:
419
- time.sleep(1)
420
- return jsonify({"status": "task started", "task_id": global_config.runner_status.get("id")})
421
- else:
422
- return render_template('experiment_run.html', script=script.script_dict, filename=filename,
423
- dot_py=exec_string, line_collection=line_collection,
424
- return_list=return_list, config_list=config_list, config_file_list=config_file_list,
425
- config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
426
- no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
427
- history=deck_list, pause_status=runner.pause_status())
154
+ if 'status' in data:
155
+ if data['status'] == "finished":
156
+ script.finalize()
157
+ utils.post_script_file(script)
158
+ return jsonify(success=True)
159
+ return jsonify(success=False)
428
160
 
429
161
 
430
- @design.route("/design/script/toggle/<stype>")
162
+ @design.route("/draft/ui-state", methods=["PATCH"])
431
163
  @login_required
432
- def toggle_script_type(stype=None):
164
+ def update_ui_state():
433
165
  """
434
- .. :quickref: Workflow Design; toggle the experimental phase for design canvas.
435
-
436
- .. http:get:: /design/script/toggle/<stype>
166
+ .. :quickref: Workflow Design; update the UI state for the design canvas.
437
167
 
438
- :status 200: and then redirects to :http:get:`/design/script`
439
-
440
- """
441
- script = utils.get_script_file()
442
- script.editing_type = stype
443
- utils.post_script_file(script)
444
- return redirect(url_for('design.experiment_builder'))
168
+ .. http:patch:: /draft/ui-state
445
169
 
170
+ Update the UI state for the design canvas, including showing code overlays, setting editing types,
171
+ and handling deck selection.
446
172
 
447
- @design.route("/updateList", methods=['POST'])
448
- @login_required
449
- def update_list():
450
- order = request.form['order']
451
- script = utils.get_script_file()
452
- script.currently_editing_order = order.split(",", len(script.currently_editing_script))
453
- script.sort_actions()
454
- exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
455
- utils.post_script_file(script)
456
- session['python_code'] = exec_string
457
-
458
- return jsonify({'success': True})
173
+ :form show_code: Whether to show the code overlay (true/false).
174
+ :form editing_type: The type of editing to set (prep, script, cleanup).
175
+ :form autofill: Whether to enable autofill for the instrument panel (true/false).
176
+ :form deck_name: The name of the deck to select.
459
177
 
178
+ :status 200: Updates the UI state and returns a success message.
179
+ """
180
+ data = request.get_json()
460
181
 
461
- @design.route("/toggle_show_code", methods=["POST"])
462
- def toggle_show_code():
463
- session["show_code"] = not session.get("show_code", False)
464
- return redirect(request.referrer or url_for("design.experiment_builder"))
182
+ if "show_code" in data:
183
+ session["show_code"] = bool(data["show_code"])
184
+ return jsonify({"success": True})
185
+ if "editing_type" in data:
186
+ stype = data.get("editing_type")
465
187
 
188
+ script = utils.get_script_file()
189
+ script.editing_type = stype
190
+ utils.post_script_file(script)
466
191
 
467
- # --------------------handle all the import/export and download/upload--------------------------
468
- @design.route("/design/clear")
192
+ # Re-render only the part of the page you want to update
193
+ design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
194
+ rendered_html = render_template("components/canvas.html", script=script, buttons_dict=design_buttons)
195
+ return jsonify({"html": rendered_html})
196
+
197
+ if "autofill" in data:
198
+ script = utils.get_script_file()
199
+ instrument = data.get("instrument", '')
200
+ autofill = data.get("autofill", False)
201
+ session['autofill'] = autofill
202
+ _, forms = _create_forms(instrument, script, autofill)
203
+ rendered_html = render_template("components/actions_panel.html", forms=forms, script=script, instrument=instrument)
204
+ return jsonify({"html": rendered_html})
205
+
206
+ if "deck_name" in data:
207
+ pkl_name = data.get('deck_name', "")
208
+ script = utils.get_script_file()
209
+ session['pseudo_deck'] = pkl_name
210
+ deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
211
+
212
+ if script.deck is None or script.isEmpty():
213
+ script.deck = pkl_name.split('.')[0]
214
+ utils.post_script_file(script)
215
+ elif script.deck and not script.deck == pkl_name.split('.')[0]:
216
+ flash(f"Choose the deck with name {script.deck}")
217
+ pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pkl_name)
218
+ pseudo_deck = utils.load_deck(pseudo_deck_path)
219
+ deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
220
+ deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
221
+ html = render_template("components/sidebar.html", history=deck_list,
222
+ defined_variables=deck_variables, local_variables = global_config.defined_variables,
223
+ block_variables=global_config.building_blocks)
224
+ return jsonify({"html": html})
225
+ return jsonify({"error": "Invalid request"}), 400
226
+
227
+
228
+ # @design.route("/draft/steps/order", methods=['POST'])
229
+ # @login_required
230
+ # def update_list():
231
+ # """
232
+ # .. :quickref: Workflow Design Steps; update the order of steps in the design canvas when reordering steps.
233
+ #
234
+ # .. http:post:: /draft/steps/order
235
+ #
236
+ # Update the order of steps in the design canvas when reordering steps.
237
+ #
238
+ # :form order: A comma-separated string representing the new order of steps.
239
+ # :status 200: Successfully updated the order of steps.
240
+ # """
241
+ # order = request.form['order']
242
+ # script = utils.get_script_file()
243
+ # script.currently_editing_order = order.split(",", len(script.currently_editing_script))
244
+ # script.sort_actions()
245
+ # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
246
+ # utils.post_script_file(script)
247
+ # session['python_code'] = exec_string
248
+ #
249
+ # return jsonify({'success': True})
250
+
251
+
252
+
253
+ @design.route("/draft", methods=['DELETE'])
469
254
  @login_required
470
- def clear():
255
+ def clear_draft():
471
256
  """
472
257
  .. :quickref: Workflow Design; clear the design canvas.
473
258
 
474
- .. http:get:: /design/clear
259
+ .. http:delete:: /draft
475
260
 
476
- :form prompt: user's prompt
477
- :status 200: clear canvas and then redirects to :http:get:`/design/script`
261
+ :status 200: clear canvas
478
262
  """
479
263
  deck = global_config.deck
480
- pseudo_name = session.get("pseudo_deck", "")
481
264
  if deck:
482
265
  deck_name = os.path.splitext(os.path.basename(deck.__file__))[
483
266
  0] if deck.__name__ == "__main__" else deck.__name__
484
- elif pseudo_name:
485
- deck_name = pseudo_name
486
267
  else:
487
- deck_name = ''
488
- script = Script(deck=deck_name, author=session.get('username'))
268
+ deck_name = session.get("pseudo_deck", "")
269
+ script = Script(deck=deck_name, author=current_user.get_id())
489
270
  utils.post_script_file(script)
490
- return redirect(url_for("design.experiment_builder"))
491
-
492
-
493
- @design.route("/design/import/pseudo", methods=['POST'])
494
- @login_required
495
- def import_pseudo():
496
- """
497
- .. :quickref: Workflow Design; Import pseudo deck from deck history
271
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
272
+ session['python_code'] = exec_string
273
+ return jsonify({'success': True})
498
274
 
499
- .. http:post:: /design/import/pseudo
500
275
 
501
- :form pkl_name: pseudo deck name
502
- :status 302: load pseudo deck and then redirects to :http:get:`/design/script`
503
- """
504
- pkl_name = request.form.get('pkl_name')
505
- script = utils.get_script_file()
506
- session['pseudo_deck'] = pkl_name
507
276
 
508
- if script.deck is None or script.isEmpty():
509
- script.deck = pkl_name.split('.')[0]
510
- utils.post_script_file(script)
511
- elif script.deck and not script.deck == pkl_name.split('.')[0]:
512
- flash(f"Choose the deck with name {script.deck}")
513
- return redirect(url_for("design.experiment_builder"))
514
277
 
515
278
 
516
- @design.route('/design/uploads', methods=['POST'])
517
- @login_required
518
- def upload():
279
+ @design.route("/draft/submit_python", methods=["POST"])
280
+ def submit_script():
519
281
  """
520
- .. :quickref: Workflow Execution; upload a workflow config file (.CSV)
282
+ .. :quickref: Workflow Design; convert Python to workflow script
521
283
 
522
- .. http:post:: /design/uploads
523
-
524
- :form file: workflow CSV config file
525
- :status 302: save csv file and then redirects to :http:get:`/design/campaign`
526
- """
527
- if request.method == "POST":
528
- f = request.files['file']
529
- if 'file' not in request.files:
530
- flash('No file part')
531
- if f.filename.split('.')[-1] == "csv":
532
- filename = secure_filename(f.filename)
533
- f.save(os.path.join(current_app.config['CSV_FOLDER'], filename))
534
- session['config_file'] = filename
535
- return redirect(url_for("design.experiment_run"))
536
- else:
537
- flash("Config file is in csv format")
538
- return redirect(url_for("design.experiment_run"))
284
+ .. http:post:: /draft/submit_python
539
285
 
286
+ Convert a Python script to a workflow script and save it in the database.
540
287
 
541
- @design.route('/design/workflow/download/<filename>')
542
- @login_required
543
- def download_results(filename):
288
+ :form workflow_name: workflow name
289
+ :form script: main script
290
+ :form prep: prep script
291
+ :form cleanup: post script
292
+ :status 200: clear canvas
544
293
  """
545
- .. :quickref: Workflow Design; download a workflow data file
546
-
547
- .. http:get:: /design/workflow/download/<filename>
294
+ deck = global_config.deck
295
+ deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
296
+ script = Script(author=current_user.get_id(), deck=deck_name)
297
+ script_collection = request.get_json()
298
+ workflow_name = script_collection.pop("workflow_name")
299
+ script.python_script = script_collection
300
+ # todo check script format
301
+ script.name = workflow_name
302
+ result = {}
303
+ for stype, py_str in script_collection.items():
304
+ try:
305
+ card = convert_to_cards(py_str)
306
+ script.script_dict[stype] = card
307
+ result[stype] = "success"
308
+ except Exception as e:
309
+ result[stype] = f"failed to transcript, but function can still run. error: {str(e)}"
310
+ utils.post_script_file(script)
311
+ status = publish()
312
+ return jsonify({"script": result, "db": status}), 200
548
313
 
549
- """
550
- filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
551
- return send_file(os.path.abspath(filepath), as_attachment=True)
552
314
 
553
315
 
554
- @design.route('/design/load_json', methods=['POST'])
316
+ @design.post("/draft/instruments/<string:instrument>")
555
317
  @login_required
556
- def load_json():
318
+ def methods_handler(instrument: str = ''):
557
319
  """
558
- .. :quickref: Workflow Design Ext; upload a workflow design file (.JSON)
320
+ .. :quickref: Workflow Design; handle methods of a specific instrument
559
321
 
560
- .. http:post:: /load_json
322
+ .. http:post:: /draft/instruments/<string:instrument>
561
323
 
562
- :form file: workflow design JSON file
563
- :status 302: load pseudo deck and then redirects to :http:get:`/design/script`
564
- """
565
- if request.method == "POST":
566
- f = request.files['file']
567
- if 'file' not in request.files:
568
- flash('No file part')
569
- if f.filename.endswith("json"):
570
- script_dict = json.load(f)
571
- utils.post_script_file(script_dict, is_dict=True)
572
- else:
573
- flash("Script file need to be JSON file")
574
- return redirect(url_for("design.experiment_builder"))
575
-
576
-
577
- @design.route('/design/script/download/<filetype>')
578
- @login_required
579
- def download(filetype):
580
- """
581
- .. :quickref: Workflow Design Ext; download a workflow design file
582
-
583
- .. http:get:: /design/script/download/<filetype>
324
+ Add methods for a specific instrument in the workflow design.
584
325
 
326
+ :param instrument: The name of the instrument to handle methods for.
327
+ :type instrument: str
328
+ :status 200: Render the methods for the specified instrument.
585
329
  """
586
330
  script = utils.get_script_file()
587
- run_name = script.name if script.name else "untitled"
588
- if filetype == "configure":
589
- filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
590
- with open(filepath, 'w', newline='') as f:
591
- writer = csv.writer(f)
592
- cfg, cfg_types = script.config("script")
593
- writer.writerow(cfg)
594
- writer.writerow(list(cfg_types.values()))
595
- elif filetype == "script":
596
- script.sort_actions()
597
- json_object = json.dumps(script.as_dict())
598
- filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}.json")
599
- with open(filepath, "w") as outfile:
600
- outfile.write(json_object)
601
- elif filetype == "python":
602
- filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
603
- else:
604
- return "Unsupported file type", 400
605
- return send_file(os.path.abspath(filepath), as_attachment=True)
606
-
607
-
608
- @design.route("/design/step/edit/<uuid>", methods=['GET', 'POST'])
609
- @login_required
610
- def edit_action(uuid: str):
611
- """
612
- .. :quickref: Workflow Design; edit parameters of an action step on canvas
331
+ pseudo_deck_name = session.get('pseudo_deck', '')
332
+ pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
333
+ off_line = current_app.config["OFF_LINE"]
334
+ pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
335
+ autofill = session.get('autofill', False)
613
336
 
614
- .. http:get:: /design/step/edit/<uuid>
337
+ functions, forms = _create_forms(instrument, script, autofill, pseudo_deck)
615
338
 
616
- Load parameter form of an action step
339
+ success = True
340
+ msg = ""
341
+ request.form
342
+ if "hidden_name" in request.form:
343
+ deck_snapshot = global_config.deck_snapshot
344
+ block_snapshot = global_config.building_blocks
345
+ method_name = request.form.get("hidden_name", None)
346
+ form = forms.get(method_name) if forms else None
347
+ insert_position = request.form.get("drop_target_id", None)
617
348
 
618
- .. http:post:: /design/step/edit/<uuid>
349
+ if form:
350
+ kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
351
+ if form.validate_on_submit():
352
+ function_name = kwargs.pop("hidden_name")
353
+ batch_action = kwargs.pop("batch_action", False)
354
+ save_data = kwargs.pop('return', '')
355
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[function_name])
619
356
 
620
- :param uuid: The step's uuid
621
- :type uuid: str
357
+ # todo
358
+ # print(primitive_arg_types)
622
359
 
623
- :form dynamic form: workflow step dynamic inputs
624
- :status 302: save changes and then redirects to :http:get:`/design/script`
625
- """
626
- script = utils.get_script_file()
627
- action = script.find_by_uuid(uuid)
628
- session['edit_action'] = action
629
-
630
- if request.method == "POST" and action is not None:
631
- forms = create_form_from_action(action, script=script)
632
- if "back" not in request.form:
633
- kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
634
- # print(kwargs)
635
- if forms and forms.validate_on_submit():
636
- save_as = kwargs.pop('return', '')
360
+ script.eval_list(kwargs, primitive_arg_types)
637
361
  kwargs = script.validate_variables(kwargs)
638
- # try:
639
- script.update_by_uuid(uuid=uuid, args=kwargs, output=save_as)
640
- # except Exception as e:
362
+ coroutine = False
363
+ if instrument.startswith("deck") and deck_snapshot:
364
+ coroutine = deck_snapshot[instrument][function_name].get("coroutine", False)
365
+ elif instrument.startswith("blocks") and block_snapshot:
366
+ coroutine = block_snapshot[instrument][function_name].get("coroutine", False)
367
+ action = {"instrument": instrument, "action": function_name,
368
+ "args": kwargs,
369
+ "return": save_data,
370
+ 'arg_types': primitive_arg_types,
371
+ "coroutine": coroutine,
372
+ "batch_action": batch_action,
373
+ }
374
+ script.add_action(action=action, insert_position=insert_position)
641
375
  else:
642
- flash(forms.errors)
643
- session.pop('edit_action')
644
- return redirect(url_for('design.experiment_builder'))
645
-
646
-
647
- @design.route("/design/step/delete/<id>")
648
- @login_required
649
- def delete_action(id: int):
650
- """
651
- .. :quickref: Workflow Design; delete an action step on canvas
652
-
653
- .. http:get:: /design/step/delete/<id>
654
-
655
- :param id: The step number id
656
- :type id: int
657
-
658
- :status 302: save changes and then redirects to :http:get:`/design/script`
659
- """
660
- back = request.referrer
661
- script = utils.get_script_file()
662
- script.delete_action(id)
376
+ msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
377
+ success = False
378
+ elif "builtin_name" in request.form:
379
+ function_name = request.form.get("builtin_name")
380
+ form = forms.get(function_name) if forms else None
381
+ insert_position = request.form.get("drop_target_id", None)
382
+ if form:
383
+ kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
384
+ if form.validate_on_submit():
385
+ logic_type = kwargs.pop('builtin_name')
386
+ if 'variable' in kwargs:
387
+ try:
388
+ script.add_variable(insert_position=insert_position, **kwargs)
389
+ except ValueError:
390
+ success = False
391
+ msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
392
+ else:
393
+ script.add_logic_action(logic_type=logic_type, insert_position=insert_position, **kwargs)
394
+ else:
395
+ success = False
396
+ msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
397
+ elif "workflow_name" in request.form:
398
+ workflow_name = request.form.get("workflow_name")
399
+ form = forms.get(workflow_name) if forms else None
400
+ insert_position = request.form.get("drop_target_id", None)
401
+ if form:
402
+ kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
403
+ if form.validate_on_submit():
404
+ save_data = kwargs.pop('return', '')
405
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[workflow_name])
406
+ script.eval_list(kwargs, primitive_arg_types)
407
+ kwargs = script.validate_variables(kwargs)
408
+ action = {"instrument": instrument, "action": workflow_name,
409
+ "args": kwargs,
410
+ "return": save_data,
411
+ 'arg_types': primitive_arg_types}
412
+ script.add_action(action=action, insert_position=insert_position)
413
+ script.add_workflow(**kwargs, insert_position=insert_position)
414
+ else:
415
+ success = False
416
+ msg = [f"{field}: {', '.join(messages)}" for field, messages in form.errors.items()]
663
417
  utils.post_script_file(script)
664
- return redirect(back)
418
+ #TODO
419
+ try:
420
+ exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
421
+ except Exception as e:
422
+ exec_string = {}
423
+ msg = f"Compilation failed: {str(e)}"
424
+ # exec_string = script.compile(current_app.config['SCRIPT_FOLDER'])
425
+ session['python_code'] = exec_string
426
+ design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
427
+ html = render_template("components/canvas_main.html", script=script, buttons_dict=design_buttons)
428
+ return jsonify({"html": html, "success": success, "error": msg})
665
429
 
666
430
 
667
- @design.route("/design/step/duplicate/<id>")
431
+ @design.get("/draft/instruments", strict_slashes=False)
432
+ @design.get("/draft/instruments/<string:instrument>")
668
433
  @login_required
669
- def duplicate_action(id: int):
434
+ def get_operation_sidebar(instrument: str = ''):
670
435
  """
671
- .. :quickref: Workflow Design; duplicate an action step on canvas
436
+ .. :quickref: Workflow Design; handle methods of a specific instrument
672
437
 
673
- .. http:get:: /design/step/duplicate/<id>
438
+ .. http:get:: /draft/instruments/<string:instrument>
674
439
 
675
- :param id: The step number id
676
- :type id: int
440
+ :param instrument: The name of the instrument to handle methods for.
441
+ :type instrument: str
677
442
 
678
- :status 302: save changes and then redirects to :http:get:`/design/script`
443
+ :status 200: Render the methods for the specified instrument.
679
444
  """
680
- back = request.referrer
681
445
  script = utils.get_script_file()
682
- script.duplicate_action(id)
683
- utils.post_script_file(script)
684
- return redirect(back)
685
-
686
-
687
- # ---- HTTP API Endpoints ----
688
-
689
- @design.route("/api/runner/status", methods=["GET"])
690
- def runner_status():
691
- """
692
- .. :quickref: Workflow Design; get the execution status
693
-
694
- .. http:get:: /api/runner/status
695
-
696
- :status 200: status
697
- """
698
- runner_busy = global_config.runner_lock.locked()
699
- status = {"busy": runner_busy}
700
- task_status = global_config.runner_status
701
- current_step = {}
702
- # print(task_status)
703
- if task_status is not None:
704
- task_type = task_status["type"]
705
- task_id = task_status["id"]
706
- if task_type == "task":
707
- step = SingleStep.query.get(task_id)
708
- current_step = step.as_dict()
709
- if task_type == "workflow":
710
- workflow = WorkflowRun.query.get(task_id)
711
- if workflow is not None:
712
- latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(WorkflowStep.start_time.desc()).first()
713
- if latest_step is not None:
714
- current_step = latest_step.as_dict()
715
- status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
716
- status["current_task"] = current_step
717
- return jsonify(status), 200
718
-
719
-
720
-
721
- @design.route("/api/runner/abort_pending", methods=["POST"])
722
- def api_abort_pending():
723
- """
724
- .. :quickref: Workflow Design; abort pending action(s) during execution
725
-
726
- .. http:get:: /api/runner/abort_pending
727
-
728
- :status 200: {"status": "ok"}
729
- """
730
- abort_pending()
731
- return jsonify({"status": "ok"}), 200
732
-
733
- @design.route("/api/runner/abort_current", methods=["POST"])
734
- def api_abort_current():
735
- """
736
- .. :quickref: Workflow Design; abort right after current action during execution
737
-
738
- .. http:get:: /api/runner/abort_current
739
-
740
- :status 200: {"status": "ok"}
741
- """
742
- abort_current()
743
- return jsonify({"status": "ok"}), 200
744
-
745
- @design.route("/api/runner/pause", methods=["POST"])
746
- def api_pause():
747
- """
748
- .. :quickref: Workflow Design; pause during execution
749
-
750
- .. http:get:: /api/runner/pause
751
-
752
- :status 200: {"status": "ok"}
753
- """
754
- msg = pause()
755
- return jsonify({"status": "ok", "pause_status": msg}), 200
756
-
757
- @design.route("/api/runner/retry", methods=["POST"])
758
- def api_retry():
759
- """
760
- .. :quickref: Workflow Design; retry when error occur during execution
761
-
762
- .. http:get:: /api/runner/retry
763
-
764
- :status 200: {"status": "ok"}
765
- """
766
- retry()
767
- return jsonify({"status": "ok, retrying failed step"}), 200
446
+ pseudo_deck_name = session.get('pseudo_deck', '')
447
+ pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
448
+ off_line = current_app.config["OFF_LINE"]
449
+ pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
450
+ autofill = session.get('autofill', False)
768
451
 
452
+ functions, forms = _create_forms(instrument, script, autofill, pseudo_deck)
769
453
 
770
- @design.route("/api/design/submit", methods=["POST"])
771
- def submit_script():
772
- """
773
- .. :quickref: Workflow Design; submit script
454
+ if instrument:
455
+ html = render_template("components/sidebar.html", forms=forms, instrument=instrument, script=script)
456
+ else:
457
+ pseudo_deck_name = session.get('pseudo_deck', '')
458
+ pseudo_deck_path = os.path.join(current_app.config["DUMMY_DECK"], pseudo_deck_name)
459
+ off_line = current_app.config["OFF_LINE"]
460
+ pseudo_deck = utils.load_deck(pseudo_deck_path) if off_line and pseudo_deck_name else None
461
+ if off_line and pseudo_deck is None:
462
+ flash("Choose available deck below.")
463
+ deck_list = utils.available_pseudo_deck(current_app.config["DUMMY_DECK"])
464
+ if not off_line:
465
+ deck_variables = list(global_config.deck_snapshot.keys())
466
+ else:
467
+ deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
468
+ deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
469
+ # edit_action_info = session.get("edit_action")
470
+ html = render_template("components/sidebar.html", off_line=off_line, history=deck_list,
471
+ defined_variables=deck_variables,
472
+ local_variables=global_config.defined_variables,
473
+ block_variables=global_config.building_blocks,
474
+ )
475
+ return jsonify({"html": html})
774
476
 
775
- .. http:get:: /api/design/submit
776
477
 
777
- :status 200: {"status": "ok"}
778
- """
779
- deck = global_config.deck
780
- deck_name = os.path.splitext(os.path.basename(deck.__file__))[0] if deck.__name__ == "__main__" else deck.__name__
781
- script = Script(author=session.get('user'), deck=deck_name)
782
- script_collection = request.get_json()
783
- script.python_script = script_collection
784
- # todo check script format
785
- utils.post_script_file(script)
786
- return jsonify({"status": "ok"}), 200