ivoryos 1.1.0__py3-none-any.whl → 1.2.0__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.

Potentially problematic release.


This version of ivoryos might be problematic. Click here for more details.

Files changed (58) hide show
  1. ivoryos/__init__.py +12 -5
  2. ivoryos/routes/api/api.py +5 -58
  3. ivoryos/routes/control/control.py +46 -43
  4. ivoryos/routes/control/control_file.py +4 -4
  5. ivoryos/routes/control/control_new_device.py +10 -10
  6. ivoryos/routes/control/templates/controllers.html +38 -9
  7. ivoryos/routes/control/templates/controllers_new.html +1 -1
  8. ivoryos/routes/data/data.py +81 -60
  9. ivoryos/routes/data/templates/components/step_card.html +9 -3
  10. ivoryos/routes/data/templates/workflow_database.html +10 -4
  11. ivoryos/routes/design/design.py +306 -243
  12. ivoryos/routes/design/design_file.py +42 -31
  13. ivoryos/routes/design/design_step.py +132 -30
  14. ivoryos/routes/design/templates/components/action_form.html +4 -3
  15. ivoryos/routes/design/templates/components/{instrument_panel.html → actions_panel.html} +7 -5
  16. ivoryos/routes/design/templates/components/autofill_toggle.html +8 -12
  17. ivoryos/routes/design/templates/components/canvas.html +5 -14
  18. ivoryos/routes/design/templates/components/canvas_footer.html +5 -1
  19. ivoryos/routes/design/templates/components/canvas_header.html +36 -15
  20. ivoryos/routes/design/templates/components/canvas_main.html +34 -0
  21. ivoryos/routes/design/templates/components/deck_selector.html +8 -10
  22. ivoryos/routes/design/templates/components/edit_action_form.html +16 -7
  23. ivoryos/routes/design/templates/components/instruments_panel.html +66 -0
  24. ivoryos/routes/design/templates/components/modals/drop_modal.html +3 -5
  25. ivoryos/routes/design/templates/components/modals/new_script_modal.html +1 -2
  26. ivoryos/routes/design/templates/components/modals/rename_modal.html +5 -5
  27. ivoryos/routes/design/templates/components/modals/saveas_modal.html +2 -2
  28. ivoryos/routes/design/templates/components/python_code_overlay.html +26 -4
  29. ivoryos/routes/design/templates/components/sidebar.html +12 -13
  30. ivoryos/routes/design/templates/experiment_builder.html +20 -20
  31. ivoryos/routes/execute/execute.py +157 -13
  32. ivoryos/routes/execute/execute_file.py +38 -4
  33. ivoryos/routes/execute/templates/components/tab_bayesian.html +365 -114
  34. ivoryos/routes/execute/templates/components/tab_configuration.html +1 -1
  35. ivoryos/routes/library/library.py +70 -115
  36. ivoryos/routes/library/templates/library.html +27 -19
  37. ivoryos/static/js/action_handlers.js +213 -0
  38. ivoryos/static/js/db_delete.js +23 -0
  39. ivoryos/static/js/script_metadata.js +39 -0
  40. ivoryos/static/js/sortable_design.js +89 -56
  41. ivoryos/static/js/ui_state.js +113 -0
  42. ivoryos/utils/bo_campaign.py +137 -1
  43. ivoryos/utils/db_models.py +14 -5
  44. ivoryos/utils/form.py +4 -9
  45. ivoryos/utils/global_config.py +13 -1
  46. ivoryos/utils/script_runner.py +24 -5
  47. ivoryos/utils/serilize.py +203 -0
  48. ivoryos/utils/task_runner.py +4 -1
  49. ivoryos/version.py +1 -1
  50. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/METADATA +1 -1
  51. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/RECORD +54 -51
  52. ivoryos/routes/design/templates/components/action_list.html +0 -15
  53. ivoryos/routes/design/templates/components/operations_panel.html +0 -43
  54. ivoryos/routes/design/templates/components/script_info.html +0 -31
  55. ivoryos/routes/design/templates/components/scripts.html +0 -50
  56. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/LICENSE +0 -0
  57. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/WHEEL +0 -0
  58. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,5 @@
1
- {# Drop modal component #}
2
- <div class="modal fade" id="dropModal" tabindex="-1" aria-labelledby="dropModalLabel" aria-hidden="true">
3
- <div class="modal-dialog">
1
+ <div class="modal fade" id="dropModal" tabindex="-1" role="dialog">
2
+ <div class="modal-dialog" role="document">
4
3
  <div class="modal-content">
5
4
  <div class="modal-header">
6
5
  <h5 class="modal-title" id="dropModalLabel">Configure Action</h5>
@@ -8,7 +7,6 @@
8
7
  </div>
9
8
  <div class="modal-body">
10
9
  <p>Drop Position ID: <strong id="modalDropTarget"></strong></p>
11
- <!-- Form will be dynamically inserted here -->
12
10
  <div id="modalFormFields"></div>
13
11
  </div>
14
12
  <div class="modal-footer">
@@ -16,4 +14,4 @@
16
14
  </div>
17
15
  </div>
18
16
  </div>
19
- </div>
17
+ </div>
@@ -11,8 +11,7 @@
11
11
  </div>
12
12
  <div class="modal-footer">
13
13
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Continue editing </button>
14
- <a role="button" class="btn btn-primary" href="{{url_for('design.clear')}}"> Already saved, clear all </a>
15
- </div>
14
+ <button class="btn btn-danger" onclick="clearDraft()">Already saved, clear all</button> </div>
16
15
  </div>
17
16
  </div>
18
17
  </div>
@@ -6,16 +6,16 @@
6
6
  <h1 class="modal-title fs-5" id="renameModal">Rename your script</h1>
7
7
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
8
8
  </div>
9
- <form method="POST" name="run_name" action="{{ url_for('library.edit_run_name') }}">
10
- <div class="modal-body">
9
+ <form name="run_name" onsubmit="editScriptName(event)">
10
+ <div class="modal-body">
11
11
  <div class="input-group mb-3">
12
- <label class="input-group-text" for="run_name">Run Name</label>
13
- <input class="form-control" type="text" name="run_name" id="run_name" placeholder="{{script['name']}}" required="required">
12
+ <label class="input-group-text" for="new-name">Run Name</label>
13
+ <input class="form-control" type="text" name="new-name" id="new-name" value="{{ script['name'] }}" required="required">
14
14
  </div>
15
15
  </div>
16
16
  <div class="modal-footer">
17
17
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
18
- <button type="submit" class="btn btn-primary"> Save </button>
18
+ <button type="submit" class="btn btn-primary"> Save</button>
19
19
  </div>
20
20
  </form>
21
21
  </div>
@@ -6,7 +6,7 @@
6
6
  <h1 class="modal-title fs-5" id="saveasModal">Save your script as </h1>
7
7
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
8
8
  </div>
9
- <form method="POST" name="run_name" action="{{ url_for('library.save_as') }}">
9
+ <form method="POST" name="save-as" action="{{ url_for('library.save_as') }}">
10
10
  <div class="modal-body">
11
11
  <div class="input-group mb-3">
12
12
  <label class="input-group-text" for="run_name">Run Name</label>
@@ -19,7 +19,7 @@
19
19
  </div>
20
20
  <div class="modal-footer">
21
21
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> Close </button>
22
- <button type="submit" class="btn btn-primary"> Save </button>
22
+ <button type="submit" class="btn btn-primary" > Save </button>
23
23
  </div>
24
24
  </form>
25
25
  </div>
@@ -1,17 +1,39 @@
1
1
  {# Python code overlay component #}
2
+ {#{% if session.get('show_code') %}#}
3
+ <style>
4
+ .code-overlay {
5
+ position: fixed;
6
+ right: 0;
7
+ top: 180px;
8
+ height: 100vh;
9
+ width: 400px;
10
+ z-index: 1000;
11
+ transition: transform 0.3s ease;
12
+ transform: translateX(100%);
13
+ }
14
+
15
+ .code-overlay.show {
16
+ transform: translateX(0);
17
+ }
18
+ </style>
19
+ <script>hljs.highlightAll();</script>
2
20
  {% if session.get('show_code') %}
3
21
  <div id="pythonCodeOverlay" class="code-overlay bg-light border-start show">
22
+ {% else %}
23
+ <div id="pythonCodeOverlay" class="code-overlay bg-light border-start">
24
+ {% endif %}
4
25
  <div class="overlay-header d-flex justify-content-between align-items-center px-3 py-2 border-bottom">
5
26
  <strong>Python Code</strong>
6
- <button class="btn btn-sm btn-outline-secondary" onclick="toggleCodeOverlay(false)">
27
+ <button class="btn btn-sm btn-outline-secondary" onclick="toggleCodeOverlay()">
7
28
  <i class="bi bi-x-lg"></i>
8
29
  </button>
9
30
  </div>
10
31
  <div class="overlay-content p-3">
11
- {% for stype, script in session['python_code'].items() %}
32
+ {% for stype, script in session.get('python_code', {}).items() %}
12
33
  <pre><code class="language-python">{{ script }}</code></pre>
13
34
  {% endfor %}
14
- <a href="{{ url_for('design.design_files.download', filetype='python') }}">Download <i class="bi bi-download"></i></a>
35
+ <a href="{{ url_for('design.design_files.download_python', filetype='python') }}">
36
+ Download <i class="bi bi-download"></i>
37
+ </a>
15
38
  </div>
16
39
  </div>
17
- {% endif %}
@@ -1,16 +1,15 @@
1
1
  {# Sidebar component for experiment builder #}
2
- <div class="col-md-3 scroll-column">
3
- {# select deck if this is online#}
4
- {% if off_line %}
5
- {% include 'components/deck_selector.html' %}
6
- {% endif %}
7
-
8
- {# edit action #}
9
- {% if session["edit_action"] %}
10
- {% include 'components/edit_action_form.html' %}
11
- {% elif instrument %}
12
- {% include 'components/instrument_panel.html' %}
2
+ <div class="instrument-panel" id="instrument-panel">
3
+ {% if instrument %}
4
+ <div class="instrument-methods" id="instrument-methods">
5
+ {% include 'components/actions_panel.html' %}
6
+ </div>
13
7
  {% else %}
14
- {% include 'components/operations_panel.html' %}
8
+ {# select deck if this is online#}
9
+ {% if off_line %}
10
+ {% include 'components/deck_selector.html' %}
11
+ {% endif %}
12
+
13
+ {% include 'components/instruments_panel.html' %}
15
14
  {% endif %}
16
- </div>
15
+ </div>
@@ -3,23 +3,7 @@
3
3
 
4
4
  {% block body %}
5
5
  {# overlay block for text-to-code gen #}
6
- <style>
7
- .code-overlay {
8
- position: absolute;
9
- top: 0;
10
- right: -50%;
11
- height: 100%;
12
- width: 50%;
13
- z-index: 100;
14
- background: #f8f9fa;
15
- box-shadow: -2px 0 6px rgba(0, 0, 0, 0.1);
16
- transition: right 0.3s ease;
17
- overflow-y: auto;
18
- }
19
- .code-overlay.show {
20
- right: 0;
21
- }
22
- </style>
6
+
23
7
  <div id="overlay" class="overlay">
24
8
  <div>
25
9
  <h3 id="overlay-text">Generating design, please wait...</h3>
@@ -28,14 +12,30 @@
28
12
  </div>
29
13
 
30
14
  <div class="row">
31
- {% include 'components/sidebar.html' %}
32
- {% include 'components/canvas.html' %}
15
+ <div class="col-md-3 scroll-column" id="sidebar-wrapper">
16
+ {% include 'components/sidebar.html' %}
17
+ </div>
18
+ <div class="col-md-9 scroll-column" id="canvas-wrapper">
19
+ {% include 'components/canvas.html' %}
20
+ </div>
21
+
33
22
  </div>
34
23
 
35
24
  {# Include all modals #}
36
25
  {% include 'components/modals.html' %}
37
26
 
38
27
  {# Include all scripts #}
39
- {% include 'components/scripts.html' %}
28
+ <script>
29
+ const updateListUrl = "{{ url_for('design.design_steps.update_list') }}";
30
+ const scriptUIStateUrl = "{{ url_for('design.update_ui_state') }}";
31
+ const scriptMetaUrl = "{{ url_for('design.update_script_meta') }}";
32
+ const scriptStepUrl = `{{ url_for('design.design_steps.get_step', uuid=0) }}`;
33
+ const scriptStepDupUrl = `{{ url_for('design.design_steps.duplicate_action', uuid=0) }}`;
34
+ const scriptDeleteUrl = "{{ url_for('design.clear_draft') }}";
35
+ </script>
36
+ <script src="{{ url_for('static', filename='js/sortable_design.js') }}"></script>
37
+ <script src="{{ url_for('static', filename='js/action_handlers.js') }}"></script>
38
+ <script src="{{ url_for('static', filename='js/script_metadata.js') }}"></script>
39
+ <script src="{{ url_for('static', filename='js/ui_state.js') }}"></script>
40
40
 
41
41
  {% endblock %}
@@ -8,13 +8,14 @@ from flask_login import login_required
8
8
 
9
9
  from ivoryos.routes.execute.execute_file import files
10
10
  from ivoryos.utils import utils
11
+ from ivoryos.utils.bo_campaign import parse_optimization_form
12
+ from ivoryos.utils.db_models import SingleStep, WorkflowRun, WorkflowStep
11
13
  from ivoryos.utils.global_config import GlobalConfig
12
- from ivoryos.utils.form import create_action_button, format_name, create_form_from_pseudo, \
13
- create_form_from_action, create_all_builtin_forms
14
+ from ivoryos.utils.form import create_action_button
14
15
 
15
16
  from werkzeug.utils import secure_filename
16
17
 
17
- from ivoryos.socket_handlers import runner
18
+ from ivoryos.socket_handlers import runner, retry, pause, abort_pending, abort_current
18
19
 
19
20
  execute = Blueprint('execute', __name__, template_folder='templates')
20
21
 
@@ -23,19 +24,19 @@ execute.register_blueprint(files)
23
24
  global_config = GlobalConfig()
24
25
 
25
26
 
26
- @execute.route("/campaign", methods=['GET', 'POST'])
27
+ @execute.route("/executions/config", methods=['GET', 'POST'])
27
28
  @login_required
28
29
  def experiment_run():
29
30
  """
30
- .. :quickref: Workflow Execution; Execute/iterate the workflow
31
+ .. :quickref: Workflow Execution Config; Execute/iterate the workflow
31
32
 
32
- .. http:get:: /design/campaign
33
+ .. http:get:: /executions/config
33
34
 
34
- Compile the workflow and load the experiment execution interface.
35
+ Load the experiment execution interface.
35
36
 
36
- .. http:post:: /design/campaign
37
+ .. http:post:: /executions/config
37
38
 
38
- Start workflow execution
39
+ Start workflow execution with experiment configuration.
39
40
 
40
41
  """
41
42
  deck = global_config.deck
@@ -45,7 +46,7 @@ def experiment_run():
45
46
  # script.sort_actions() # handled in update list
46
47
  off_line = current_app.config["OFF_LINE"]
47
48
  deck_list = utils.import_history(os.path.join(current_app.config["OUTPUT_FOLDER"], 'deck_history.txt'))
48
-
49
+ optimizers_schema = {k: v.get_schema() for k, v in global_config.optimizers.items()}
49
50
  design_buttons = {stype: create_action_button(script, stype) for stype in script.stypes}
50
51
  config_preview = []
51
52
  config_file_list = [i for i in os.listdir(current_app.config["CSV_FOLDER"]) if not i == ".gitkeep"]
@@ -149,13 +150,156 @@ def experiment_run():
149
150
  return_list=return_list, config_list=config_list, config_file_list=config_file_list,
150
151
  config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
151
152
  no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
152
- history=deck_list, pause_status=runner.pause_status())
153
+ history=deck_list, pause_status=runner.pause_status(), optimizer_schema=optimizers_schema)
154
+
155
+ @execute.route("/executions/campaign", methods=["POST"])
156
+ @login_required
157
+ def run_bo():
158
+ """
159
+ .. :quickref: Workflow Execution; run Bayesian Optimization
160
+ Run Bayesian Optimization with the given parameters and objectives.
161
+
162
+ .. http:post:: /executions/campaign
163
+
164
+ :form repeat: number of iterations to run
165
+ :form optimizer_type: type of optimizer to use
166
+ :form existing_data: existing data to use for optimization
167
+ :form parameters: parameters for optimization
168
+ :form objectives: objectives for optimization
169
+ TODO: merge to experiment_run or not, add more details about the form fields and their expected values.
170
+ """
171
+ script = utils.get_script_file()
172
+ run_name = script.name if script.name else "untitled"
173
+ payload = request.form.to_dict()
174
+ repeat = payload.pop("repeat", None)
175
+ optimizer_type = payload.pop("optimizer_type", None)
176
+ existing_data = payload.pop("existing_data", None)
177
+ parameters, objectives, steps = parse_optimization_form(payload)
178
+ try:
179
+ datapath = current_app.config["DATA_FOLDER"]
180
+ run_name = script.validate_function_name(run_name)
181
+ Optimizer = global_config.optimizers.get(optimizer_type, None)
182
+ if not Optimizer:
183
+ raise ValueError(f"Optimizer {optimizer_type} is not supported or not found.")
184
+ optimizer = Optimizer(experiment_name=run_name, parameter_space=parameters, objective_config=objectives,
185
+ optimizer_config=steps)
186
+ runner.run_script(script=script, run_name=run_name, optimizer=optimizer,
187
+ logger=g.logger, socketio=g.socketio, repeat_count=repeat,
188
+ output_path=datapath, compiled=False, history=existing_data,
189
+ current_app=current_app._get_current_object()
190
+ )
191
+
192
+ except Exception as e:
193
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
194
+ return jsonify({"error": e.__str__()})
195
+ else:
196
+ flash(e.__str__())
197
+ return redirect(url_for("execute.experiment_run"))
198
+
199
+
200
+
201
+ @execute.route("/executions/status", methods=["GET"])
202
+ def runner_status():
203
+ """
204
+ .. :quickref: Workflow Execution Control; backend runner status
205
+
206
+ get is system is busy and current task
207
+
208
+ .. http:get:: /executions/status
209
+
210
+
211
+ """
212
+ # runner = global_config.runner
213
+ runner_busy = global_config.runner_lock.locked()
214
+ status = {"busy": runner_busy}
215
+ task_status = global_config.runner_status
216
+ current_step = {}
217
+
218
+ if task_status is not None:
219
+ task_type = task_status["type"]
220
+ task_id = task_status["id"]
221
+ if task_type == "task":
222
+ # todo
223
+ step = SingleStep.query.get(task_id)
224
+ current_step = step.as_dict()
225
+ if task_type == "workflow":
226
+ workflow = WorkflowRun.query.get(task_id)
227
+ if workflow is not None:
228
+ latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(
229
+ WorkflowStep.start_time.desc()).first()
230
+ if latest_step is not None:
231
+ current_step = latest_step.as_dict()
232
+ status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
233
+ status["current_task"] = current_step
234
+ return jsonify(status), 200
235
+
236
+
237
+ @execute.route("/executions/abort/next-iteration", methods=["POST"])
238
+ def api_abort_pending():
239
+ """
240
+ .. :quickref: Workflow Execution control; abort pending workflow
153
241
 
242
+ finish the current iteration and stop pending workflow iterations
154
243
 
155
- @execute.route('/data_preview/<filename>')
244
+ .. http:get:: /executions/abort/next-iteration
245
+
246
+ """
247
+ abort_pending()
248
+ return jsonify({"status": "ok"}), 200
249
+
250
+
251
+ @execute.route("/executions/abort/next-task", methods=["POST"])
252
+ def api_abort_current():
253
+ """
254
+ .. :quickref: Workflow Execution Control; abort all pending tasks starting from the next task
255
+
256
+ finish the current task and stop all pending tasks or iterations
257
+
258
+ .. http:get:: /executions/abort/next-task
259
+
260
+ """
261
+ abort_current()
262
+ return jsonify({"status": "ok"}), 200
263
+
264
+
265
+ @execute.route("/executions/pause-resume", methods=["POST"])
266
+ def api_pause():
267
+ """
268
+ .. :quickref: Workflow Execution Control; pause and resume
269
+
270
+ pause workflow iterations or resume workflow iterations
271
+
272
+ .. http:get:: /executions/pause-resume
273
+
274
+ """
275
+ msg = pause()
276
+ return jsonify({"status": "ok", "pause_status": msg}), 200
277
+
278
+
279
+ @execute.route("/executions/retry", methods=["POST"])
280
+ def api_retry():
281
+ """
282
+ .. :quickref: Workflow Execution Control; retry the failed workflow execution step.
283
+
284
+ retry the failed workflow execution step.
285
+
286
+ .. http:get:: /executions/retry
287
+
288
+ """
289
+ retry()
290
+ return jsonify({"status": "ok, retrying failed step"}), 200
291
+
292
+
293
+ @execute.route('/files/preview/<string:filename>')
156
294
  @login_required
157
295
  def data_preview(filename):
158
- """Serve a preview of the selected data file (CSV) as JSON."""
296
+ """
297
+ .. :quickref: Workflow Execution Files; preview a workflow history file (.CSV)
298
+
299
+ Preview the contents of a workflow history file in CSV format.
300
+
301
+ .. http:get:: /files/preview/<str:filename>
302
+ """
159
303
  import csv
160
304
  import os
161
305
  from flask import abort
@@ -8,10 +8,37 @@ from ivoryos.utils import utils
8
8
  files = Blueprint('execute_files', __name__)
9
9
 
10
10
 
11
+ @files.route('/files/execution-configs')
12
+ def download_empty_config():
13
+ """
14
+ .. :quickref: Workflow Files; download an empty workflow config file (.CSV)
11
15
 
12
- @files.route('/upload/config', methods=['POST'])
16
+ .. http:get:: /files/execution-configs
17
+
18
+ :form file: workflow design CSV file
19
+ :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions/config`
20
+ """
21
+ script = utils.get_script_file()
22
+ run_name = script.name if script.name else "untitled"
23
+
24
+ filepath = os.path.join(current_app.config['SCRIPT_FOLDER'], f"{run_name}_config.csv")
25
+ with open(filepath, 'w', newline='') as f:
26
+ writer = csv.writer(f)
27
+ cfg, cfg_types = script.config("script")
28
+ writer.writerow(cfg)
29
+ writer.writerow(list(cfg_types.values()))
30
+ return send_file(os.path.abspath(filepath), as_attachment=True)
31
+
32
+ @files.route('/files/batch-configs', methods=['POST'])
13
33
  def upload():
14
- """Upload a workflow config file (.CSV)"""
34
+ """
35
+ .. :quickref: Workflow Files; upload a workflow config file (.CSV)
36
+
37
+ .. http:post:: /files/execution-configs
38
+
39
+ :form file: workflow CSV config file
40
+ :status 302: save csv file and then redirects to :http:get:`/ivoryos/executions/config`
41
+ """
15
42
  if request.method == "POST":
16
43
  f = request.files['file']
17
44
  if 'file' not in request.files:
@@ -26,9 +53,16 @@ def upload():
26
53
  return redirect(url_for("execute.experiment_run"))
27
54
 
28
55
 
29
- @files.route('/upload/history', methods=['POST'])
56
+ @files.route('/files/execution-data', methods=['POST'])
30
57
  def upload_history():
31
- """Upload a workflow history file (.CSV)"""
58
+ """
59
+ .. :quickref: Workflow Files; upload a workflow history file (.CSV)
60
+
61
+ .. http:post:: /files/execution-data
62
+
63
+ :form file: workflow history CSV file
64
+ :status 302: save csv file and then redirects to :http:get:`/ivoryos/executions/config`
65
+ """
32
66
  if request.method == "POST":
33
67
  f = request.files['historyfile']
34
68
  if 'historyfile' not in request.files: