ivoryos 1.1.0__py3-none-any.whl → 1.2.0b1__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 -113
  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.0b1.dist-info}/METADATA +1 -1
  51. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.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.0b1.dist-info}/LICENSE +0 -0
  57. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/WHEEL +0 -0
  58. {ivoryos-1.1.0.dist-info → ivoryos-1.2.0b1.dist-info}/top_level.txt +0 -0
ivoryos/__init__.py CHANGED
@@ -19,6 +19,7 @@ from ivoryos.routes.main.main import main
19
19
  from ivoryos.utils import utils
20
20
  from ivoryos.utils.db_models import db, User
21
21
  from ivoryos.utils.global_config import GlobalConfig
22
+ from ivoryos.optimizer.registry import OPTIMIZER_REGISTRY
22
23
  from ivoryos.utils.script_runner import ScriptRunner
23
24
  from ivoryos.version import __version__ as ivoryos_version
24
25
  from importlib.metadata import entry_points
@@ -42,10 +43,10 @@ app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='sta
42
43
  app.register_blueprint(main, url_prefix=url_prefix)
43
44
  app.register_blueprint(auth, url_prefix=f'{url_prefix}/{auth.name}')
44
45
  app.register_blueprint(library, url_prefix=f'{url_prefix}/{library.name}')
45
- app.register_blueprint(control, url_prefix=f'{url_prefix}/{control.name}')
46
- app.register_blueprint(design, url_prefix=f'{url_prefix}/{design.name}')
47
- app.register_blueprint(execute, url_prefix=f'{url_prefix}/{execute.name}')
48
- app.register_blueprint(data, url_prefix=f'{url_prefix}/{data.name}')
46
+ app.register_blueprint(control, url_prefix=f'{url_prefix}/instruments')
47
+ app.register_blueprint(design, url_prefix=f'{url_prefix}')
48
+ app.register_blueprint(execute, url_prefix=f'{url_prefix}')
49
+ app.register_blueprint(data, url_prefix=f'{url_prefix}')
49
50
  app.register_blueprint(api, url_prefix=f'{url_prefix}/{api.name}')
50
51
 
51
52
  @login_manager.user_loader
@@ -94,6 +95,12 @@ def create_app(config_class=None):
94
95
  def redirect_to_prefix():
95
96
  return redirect(url_for('main.index', version=ivoryos_version)) # Assuming 'index' is a route in your blueprint
96
97
 
98
+ @app.template_filter('format_name')
99
+ def format_name(name):
100
+ name = name.split(".")[-1]
101
+ text = ' '.join(word for word in name.split('_'))
102
+ return text.capitalize()
103
+
97
104
  return app
98
105
 
99
106
 
@@ -144,7 +151,7 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
144
151
  app.config["LOGGERS_PATH"] = logger_output_name or app.config["LOGGERS_PATH"] # default.log
145
152
  logger_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["LOGGERS_PATH"])
146
153
  dummy_deck_path = os.path.join(app.config["OUTPUT_FOLDER"], app.config["DUMMY_DECK"])
147
-
154
+ global_config.optimizers = OPTIMIZER_REGISTRY
148
155
  if module:
149
156
  app.config["MODULE"] = module
150
157
  app.config["OFF_LINE"] = False
ivoryos/routes/api/api.py CHANGED
@@ -7,70 +7,17 @@ from ivoryos.utils.global_config import GlobalConfig
7
7
  from ivoryos.utils.db_models import Script, WorkflowRun, SingleStep, WorkflowStep
8
8
 
9
9
  from ivoryos.socket_handlers import abort_pending, abort_current, pause, retry, runner
10
+ from ivoryos.utils.task_runner import TaskRunner
10
11
 
11
12
  api = Blueprint('api', __name__)
12
13
  global_config = GlobalConfig()
14
+ task_runner = TaskRunner()
13
15
 
14
-
15
-
16
- @api.route("/runner/status", methods=["GET"])
17
- def runner_status():
18
- """Get the execution status"""
19
- # runner = global_config.runner
20
- runner_busy = global_config.runner_lock.locked()
21
- status = {"busy": runner_busy}
22
- task_status = global_config.runner_status
23
- current_step = {}
24
-
25
- if task_status is not None:
26
- task_type = task_status["type"]
27
- task_id = task_status["id"]
28
- if task_type == "task":
29
- step = SingleStep.query.get(task_id)
30
- current_step = step.as_dict()
31
- if task_type == "workflow":
32
- workflow = WorkflowRun.query.get(task_id)
33
- if workflow is not None:
34
- latest_step = WorkflowStep.query.filter_by(workflow_id=workflow.id).order_by(
35
- WorkflowStep.start_time.desc()).first()
36
- if latest_step is not None:
37
- current_step = latest_step.as_dict()
38
- status["workflow_status"] = {"workflow_info": workflow.as_dict(), "runner_status": runner.get_status()}
39
- status["current_task"] = current_step
40
- return jsonify(status), 200
41
-
42
-
43
- @api.route("/runner/abort_pending", methods=["POST"])
44
- def api_abort_pending():
45
- """Abort pending action(s) during execution"""
46
- abort_pending()
47
- return jsonify({"status": "ok"}), 200
48
-
49
-
50
- @api.route("/runner/abort_current", methods=["POST"])
51
- def api_abort_current():
52
- """Abort right after current action during execution"""
53
- abort_current()
54
- return jsonify({"status": "ok"}), 200
55
-
56
-
57
- @api.route("/runner/pause", methods=["POST"])
58
- def api_pause():
59
- """Pause during execution"""
60
- msg = pause()
61
- return jsonify({"status": "ok", "pause_status": msg}), 200
62
-
63
-
64
- @api.route("/runner/retry", methods=["POST"])
65
- def api_retry():
66
- """Retry when error occur during execution"""
67
- retry()
68
- return jsonify({"status": "ok, retrying failed step"}), 200
69
-
16
+ #TODO: add authentication and authorization to the API endpoints
70
17
 
71
18
 
72
19
  @api.route("/control/", strict_slashes=False, methods=['GET'])
73
- @api.route("/control/<instrument>", methods=['POST'])
20
+ @api.route("/control/<string:instrument>", methods=['POST'])
74
21
  def backend_control(instrument: str=None):
75
22
  """
76
23
  .. :quickref: Backend Control; backend control
@@ -95,7 +42,7 @@ def backend_control(instrument: str=None):
95
42
  if form:
96
43
  kwargs = {field.name: field.data for field in form if field.name not in ['csrf_token', 'hidden_name']}
97
44
  wait = request.form.get("hidden_wait", "true") == "true"
98
- output = runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
45
+ output = task_runner.run_single_step(component=instrument, method=method_name, kwargs=kwargs, wait=wait,
99
46
  current_app=current_app._get_current_object())
100
47
  return jsonify(output), 200
101
48
 
@@ -1,11 +1,11 @@
1
- from flask import Blueprint, redirect, flash, request, render_template, session, current_app
1
+ from flask import Blueprint, redirect, flash, request, render_template, session, current_app, jsonify
2
2
  from flask_login import login_required
3
3
 
4
4
  from ivoryos.routes.control.control_file import control_file
5
5
  from ivoryos.routes.control.control_new_device import control_temp
6
6
  from ivoryos.routes.control.utils import post_session_by_instrument, get_session_by_instrument, find_instrument_by_name
7
7
  from ivoryos.utils.global_config import GlobalConfig
8
- from ivoryos.utils.form import create_form_from_module, format_name
8
+ from ivoryos.utils.form import create_form_from_module
9
9
  from ivoryos.utils.task_runner import TaskRunner
10
10
 
11
11
  global_config = GlobalConfig()
@@ -18,17 +18,36 @@ control.register_blueprint(control_temp)
18
18
 
19
19
 
20
20
 
21
- @control.route("/home", strict_slashes=False, methods=["GET", "POST"])
21
+ @control.route("/", strict_slashes=False, methods=["GET", "POST"])
22
+ @control.route("/<string:instrument>", strict_slashes=False, methods=["GET", "POST"])
22
23
  @login_required
23
24
  def deck_controllers():
24
25
  """
25
- Combined controllers page: sidebar with all instruments, main area with method cards for selected instrument.
26
+ .. :quickref: Direct Control; device (instruments) and methods
27
+
28
+ device home interface for listing all instruments and methods, selecting an instrument to run its methods
29
+
30
+ .. http:get:: /instruments
31
+
32
+ get all instruments for home page
33
+
34
+ .. http:get:: /instruments/<string:instrument>
35
+
36
+ get all methods of the given <instrument>
37
+
38
+ .. http:post:: /instruments/<string:instrument>
39
+
40
+ send POST request to run a method of the given <instrument>
41
+
42
+ :param instrument: instrument name, if not provided, list all instruments
43
+ :type instrument: str
44
+ :status 200: render template with instruments and methods
45
+
26
46
  """
27
47
  deck_variables = global_config.deck_snapshot.keys()
28
48
  temp_variables = global_config.defined_variables.keys()
29
49
  instrument = request.args.get('instrument')
30
50
  forms = None
31
- # format_name_fn = format_name
32
51
  if instrument:
33
52
  inst_object = find_instrument_by_name(instrument)
34
53
  _forms = create_form_from_module(sdl_module=inst_object, autofill=False, design=False)
@@ -47,12 +66,12 @@ def deck_controllers():
47
66
  form = forms.get(method_name)
48
67
  kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'} if form else {}
49
68
  if form and form.validate_on_submit():
50
- try:
51
- kwargs.pop("hidden_name", None)
52
- output = runner.run_single_step(instrument, method_name, kwargs, wait=True, current_app=current_app._get_current_object())
53
- flash(f"\nRun Success! Output value: {output}.")
54
- except Exception as e:
55
- flash(str(e))
69
+ kwargs.pop("hidden_name", None)
70
+ output = runner.run_single_step(instrument, method_name, kwargs, wait=True, current_app=current_app._get_current_object())
71
+ if output["success"]:
72
+ flash(f"\nRun Success! Output value: {output.get('output', 'None')}.")
73
+ else:
74
+ flash(f"\nRun Error! {output.get('output', 'Unknown error occurred.')}", "error")
56
75
  else:
57
76
  if form:
58
77
  flash(form.errors)
@@ -64,16 +83,15 @@ def deck_controllers():
64
83
  temp_variables=temp_variables,
65
84
  instrument=instrument,
66
85
  forms=forms,
67
- format_name=format_name,
68
86
  session=session
69
87
  )
70
88
 
71
- @control.route('/<instrument>/save-order', methods=['POST'])
89
+ @control.route('/<string:instrument>/actions/order', methods=['POST'])
72
90
  def save_order(instrument: str):
73
91
  """
74
92
  .. :quickref: Control Customization; Save functions' order
75
93
 
76
- .. http:post:: /control/save-order
94
+ .. http:post:: instruments/<string:instrument>/actions/order
77
95
 
78
96
  save function drag and drop order for the given <instrument>
79
97
 
@@ -83,47 +101,32 @@ def save_order(instrument: str):
83
101
  post_session_by_instrument('card_order', instrument, data['order'])
84
102
  return '', 204
85
103
 
86
-
87
- @control.route('/<instrument>/<function>/hide')
88
- def hide_function(instrument, function):
104
+ @control.route('/<string:instrument>/actions/<string:function>', methods=["PATCH"])
105
+ def hide_function(instrument: str, function: str):
89
106
  """
90
- .. :quickref: Control Customization; Hide function
107
+ .. :quickref: Control Customization; Toggle function visibility
91
108
 
92
- .. http:get:: //control/<instrument>/<function>/hide
109
+ .. http:patch:: /instruments/<instrument>/actions/<function>
93
110
 
94
- Hide the given <instrument> and <function>
111
+ Toggle visibility for the given <instrument> and <function>
95
112
 
96
113
  """
97
114
  back = request.referrer
115
+ data = request.get_json()
116
+ hidden = data.get('hidden', True)
98
117
  functions = get_session_by_instrument("hidden_functions", instrument)
99
118
  order = get_session_by_instrument("card_order", instrument)
100
- if function not in functions:
119
+ if hidden and function not in functions:
101
120
  functions.append(function)
102
- order.remove(function)
103
- post_session_by_instrument('hidden_functions', instrument, functions)
104
- post_session_by_instrument('card_order', instrument, order)
105
- return redirect(back)
106
-
107
-
108
- @control.route('/<instrument>/<function>/unhide')
109
- def remove_hidden(instrument: str, function: str):
110
- """
111
- .. :quickref: Control Customization; Remove a hidden function
112
-
113
- .. http:get:: /control/<instrument>/<function>/unhide
114
-
115
- Un-hide the given <instrument> and <function>
116
-
117
- """
118
- back = request.referrer
119
- functions = get_session_by_instrument("hidden_functions", instrument)
120
- order = get_session_by_instrument("card_order", instrument)
121
- if function in functions:
121
+ if function in order:
122
+ order.remove(function)
123
+ elif not hidden and function in functions:
122
124
  functions.remove(function)
123
- order.append(function)
125
+ if function not in order:
126
+ order.append(function)
124
127
  post_session_by_instrument('hidden_functions', instrument, functions)
125
128
  post_session_by_instrument('card_order', instrument, order)
126
- return redirect(back)
129
+ return jsonify(success=True, message="Visibility updated")
127
130
 
128
131
 
129
132
 
@@ -10,15 +10,15 @@ global_config = GlobalConfig()
10
10
  control_file = Blueprint('file', __name__)
11
11
 
12
12
 
13
- @control_file.route("/download/proxy", strict_slashes=False)
13
+ @control_file.route("/files/proxy", strict_slashes=False)
14
14
  @login_required
15
15
  def download_proxy():
16
16
  """
17
- .. :quickref: Direct Control; download proxy interface
17
+ .. :quickref: Direct Control Files; download proxy interface
18
18
 
19
- download proxy interface
19
+ download proxy Python interface
20
20
 
21
- .. http:get:: /control/download
21
+ .. http:get:: /files/proxy
22
22
  """
23
23
  snapshot = global_config.deck_snapshot.copy()
24
24
  class_definitions = {}
@@ -10,18 +10,18 @@ global_config = GlobalConfig()
10
10
 
11
11
  control_temp = Blueprint('temp', __name__)
12
12
 
13
- @control_temp.route("/import/module", methods=['POST'])
13
+ @control_temp.route("/new/module", methods=['POST'])
14
14
  def import_api():
15
15
  """
16
16
  .. :quickref: Advanced Features; Manually import API module(s)
17
17
 
18
18
  importing other Python modules
19
19
 
20
- .. http:post:: /control/import/module
20
+ .. http:post:: /instruments/new/module
21
21
 
22
22
  :form filepath: API (Python class) module filepath
23
23
 
24
- import the module and redirect to :http:get:`/ivoryos/control/new/`
24
+ import the module and redirect to :http:get:`/ivoryos/instruments/new/`
25
25
 
26
26
  """
27
27
  filepath = request.form.get('filepath')
@@ -45,12 +45,12 @@ def import_api():
45
45
 
46
46
 
47
47
 
48
- @control_temp.route("/import/deck", methods=['POST'])
48
+ @control_temp.route("/new/deck-python", methods=['POST'])
49
49
  def import_deck():
50
50
  """
51
51
  .. :quickref: Advanced Features; Manually import a deck
52
52
 
53
- .. http:post:: /control/import_deck
53
+ .. http:post:: /instruments/new/deck-python
54
54
 
55
55
  :form filepath: deck module filepath
56
56
 
@@ -84,20 +84,20 @@ def import_deck():
84
84
 
85
85
 
86
86
  @control_temp.route("/new/", strict_slashes=False)
87
- @control_temp.route("/new/<instrument>", methods=['GET', 'POST'])
87
+ @control_temp.route("/new/<string:instrument>", methods=['GET', 'POST'])
88
88
  @login_required
89
- def new_controller(instrument=None):
89
+ def new_controller(instrument:str=None):
90
90
  """
91
- .. :quickref: Direct Control; connect to a new device
91
+ .. :quickref: Advanced Features; connect to a new device
92
92
 
93
93
  interface for connecting a new <instrument>
94
94
 
95
- .. http:get:: /control/new/
95
+ .. http:get:: /instruments/new/
96
96
 
97
97
  :param instrument: instrument name
98
98
  :type instrument: str
99
99
 
100
- .. http:post:: /control/new/
100
+ .. http:post:: /instruments/new/
101
101
 
102
102
  :form device_name: module instance name (e.g. my_instance = MyClass())
103
103
  :form kwargs: dynamic module initialization kwargs fields
@@ -20,7 +20,7 @@
20
20
  <a class="list-group-item list-group-item-action d-flex align-items-center {% if instrument == inst %}active bg-primary text-white border-0{% else %}bg-light{% endif %}"
21
21
  href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
22
22
  style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
23
- <span class="flex-grow-1">{{ format_name(inst) }}</span>
23
+ <span class="flex-grow-1">{{ inst | format_name }}</span>
24
24
  {% if instrument == inst %}
25
25
  <span class="ms-auto">&gt;</span>
26
26
  {% endif %}
@@ -40,7 +40,7 @@
40
40
  <a class="list-group-item list-group-item-action d-flex align-items-center {% if instrument == inst %}active bg-warning text-dark border-0{% else %}bg-light{% endif %}"
41
41
  href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
42
42
  style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
43
- <span class="flex-grow-1">{{ format_name(inst) }}</span>
43
+ <span class="flex-grow-1">{{ inst | format_name }}</span>
44
44
  {% if instrument == inst %}
45
45
  <span class="ms-auto">&gt;</span>
46
46
  {% endif %}
@@ -63,7 +63,7 @@
63
63
 
64
64
  <!-- Main: Method Cards -->
65
65
  <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4" style="height: 100vh; overflow-y: auto;">
66
- {% if instrument and forms %}
66
+ {% if instrument%}
67
67
  {# <h2 class="text-secondary">{{ instrument }} controller</h2>#}
68
68
  <div class="grid-container" id="sortable-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1rem; width: 100%;">
69
69
  {% set hidden = session.get('hidden_functions', {}) %}
@@ -73,15 +73,18 @@
73
73
  <div class="card" id="{{function}}" style="margin: 0;">
74
74
  <div class="bg-white rounded shadow-sm h-100">
75
75
  <i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title='{{ form.hidden_name.description or "Docstring is not available" }}' ></i>
76
- <a style="float: right" aria-label="Close" href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"><i class="bi bi-eye-slash-fill"></i></a>
77
- <div class="form-control" style="border: none">
78
- <form role="form" method='POST' name="{{function}}" id="{{function}}" action="{{ url_for('control.deck_controllers') }}?instrument={{ instrument }}">
76
+ <a href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"
77
+ data-method="patch" data-payload='{"hidden":true}' class="toggle-visibility">
78
+ <i style="float: right;" class="bi bi-eye-slash-fill text-muted" title="Hide function"></i>
79
+ </a>
80
+ <div class="form-control" style="border: none">
81
+ <form role="form" method='POST' name="{{function}}" id="{{function}}" action="{{ url_for('control.deck_controllers') }}?instrument={{ instrument }}">
79
82
  <div class="form-group">
80
83
  {{ form.hidden_tag() }}
81
84
  {% for field in form %}
82
85
  {% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
83
86
  <div class="input-group mb-3">
84
- <label class="input-group-text">{{ field.label.text }}</label>
87
+ <label class="input-group-text">{{ field.label.text | format_name }}</label>
85
88
  {% if field.type == "SubmitField" %}
86
89
  {{ field(class="btn btn-dark") }}
87
90
  {% elif field.type == "BooleanField" %}
@@ -94,7 +97,9 @@
94
97
  {% endfor %}
95
98
  </div>
96
99
  <div class="input-group mb-3">
97
- <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">{{format_name(function)}} </button>
100
+ <button type="submit" name="{{ function }}" id="{{ function }}" class="form-control" style="background-color: #a5cece;">
101
+ {{ function | format_name }}
102
+ </button>
98
103
  </div>
99
104
  </form>
100
105
  </div>
@@ -116,13 +121,37 @@
116
121
  <div class="accordion-body">
117
122
  {% for function in hidden_instrument %}
118
123
  <div>
119
- {{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
124
+ {{ function }}
125
+ <a href="{{ url_for('control.hide_function', instrument=instrument, function=function) }}"
126
+ data-method="patch" data-payload='{"hidden":false}' class="toggle-visibility">
127
+ <i class="bi bi-eye-fill text-success" title="Show function"></i>
128
+ </a>
120
129
  </div>
121
130
  {% endfor %}
122
131
  </div>
123
132
  </div>
124
133
  </div>
125
134
  <script>
135
+
136
+ document.querySelectorAll('.toggle-visibility').forEach(el => {
137
+ el.addEventListener('click', e => {
138
+ e.preventDefault();
139
+ const url = el.getAttribute('href');
140
+ const payload = JSON.parse(el.getAttribute('data-payload') || '{}');
141
+ fetch(url, {
142
+ method: 'PATCH',
143
+ headers: {
144
+ 'Content-Type': 'application/json'
145
+ },
146
+ body: JSON.stringify(payload)
147
+ }).then(response => {
148
+ if (response.ok) {
149
+ location.reload(); // or update the DOM directly
150
+ }
151
+ });
152
+ });
153
+ });
154
+
126
155
  const saveOrderUrl = `{{ url_for('control.save_order', instrument=instrument) }}`;
127
156
  const buttonIds = {{ session['card_order'][instrument] | tojson }};
128
157
  </script>
@@ -31,7 +31,7 @@
31
31
  <!-- Connecting Device -->
32
32
  <div class="col-xl-5 col-lg-5 col-md-6 mb-4">
33
33
  {% if device %}
34
- {{ device }}
34
+ {# {{ device }}#}
35
35
  <div class="card shadow-sm mb-4">
36
36
  <div class="card-header">
37
37
  <h5 class="mb-0">Connecting</h5>
@@ -1,21 +1,22 @@
1
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
1
+ import os
2
+
3
+ from flask import Blueprint, redirect, url_for, request, render_template, current_app, jsonify, send_file
2
4
  from flask_login import login_required
3
5
 
4
- from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
5
- from ivoryos.utils.utils import get_script_file, post_script_file
6
+ from ivoryos.utils.db_models import db, WorkflowRun, WorkflowStep
6
7
 
7
8
  data = Blueprint('data', __name__, template_folder='templates')
8
9
 
9
10
 
10
11
 
11
- @data.route('/all')
12
+ @data.route('/executions/records')
12
13
  def list_workflows():
13
14
  """
14
- .. :quickref: Database; list all workflow logs
15
+ .. :quickref: Workflow Execution Database; list all workflow execution records
15
16
 
16
- list all workflow logs
17
+ list all workflow execution records
17
18
 
18
- .. http:get:: /database/workflows/
19
+ .. http:get:: /executions/records
19
20
 
20
21
  """
21
22
  query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
@@ -36,73 +37,93 @@ def list_workflows():
36
37
  return render_template('workflow_database.html', workflows=workflows)
37
38
 
38
39
 
39
- @data.route("/get/<int:workflow_id>")
40
- def get_workflow_steps(workflow_id:int):
40
+ @data.get("/executions/records/<int:workflow_id>")
41
+ def workflow_logs(workflow_id:int):
41
42
  """
42
- .. :quickref: Database; list all workflow logs
43
+ .. :quickref: Workflow Data Database; get workflow data, steps, and logs
43
44
 
44
- list all workflow logs
45
+ get workflow data logs by workflow id
45
46
 
46
- .. http:get:: /database/workflows/<int:workflow_id>
47
+ .. http:get:: /executions/<int:workflow_id>
47
48
 
49
+ :param workflow_id: workflow id
50
+ :type workflow_id: int
48
51
  """
49
- workflow = db.session.get(WorkflowRun, workflow_id)
50
- steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
51
-
52
- # Use full objects for template rendering
53
- grouped = {
54
- "prep": [],
55
- "script": {},
56
- "cleanup": [],
57
- }
58
-
59
- # Use dicts for JSON response
60
- grouped_json = {
61
- "prep": [],
62
- "script": {},
63
- "cleanup": [],
64
- }
65
-
66
- for step in steps:
67
- step_dict = step.as_dict()
68
-
69
- if step.phase == "prep":
70
- grouped["prep"].append(step)
71
- grouped_json["prep"].append(step_dict)
72
-
73
- elif step.phase == "script":
74
- grouped["script"].setdefault(step.repeat_index, []).append(step)
75
- grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
76
-
77
- elif step.phase == "cleanup" or step.method_name == "stop":
78
- grouped["cleanup"].append(step)
79
- grouped_json["cleanup"].append(step_dict)
80
-
81
- if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
82
- return jsonify({
83
- "workflow_info": workflow.as_dict(),
84
- "steps": grouped_json,
85
- })
86
- else:
87
- return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
88
-
89
52
 
90
- @data.route("/delete/<int:workflow_id>")
53
+ if request.method == 'GET':
54
+ workflow = db.session.get(WorkflowRun, workflow_id)
55
+ steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).order_by(WorkflowStep.start_time).all()
56
+
57
+ # Use full objects for template rendering
58
+ grouped = {
59
+ "prep": [],
60
+ "script": {},
61
+ "cleanup": [],
62
+ }
63
+
64
+ # Use dicts for JSON response
65
+ grouped_json = {
66
+ "prep": [],
67
+ "script": {},
68
+ "cleanup": [],
69
+ }
70
+
71
+ for step in steps:
72
+ step_dict = step.as_dict()
73
+
74
+ if step.phase == "prep":
75
+ grouped["prep"].append(step)
76
+ grouped_json["prep"].append(step_dict)
77
+
78
+ elif step.phase == "script":
79
+ grouped["script"].setdefault(step.repeat_index, []).append(step)
80
+ grouped_json["script"].setdefault(step.repeat_index, []).append(step_dict)
81
+
82
+ elif step.phase == "cleanup" or step.method_name == "stop":
83
+ grouped["cleanup"].append(step)
84
+ grouped_json["cleanup"].append(step_dict)
85
+
86
+ if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
87
+ return jsonify({
88
+ "workflow_info": workflow.as_dict(),
89
+ "steps": grouped_json,
90
+ })
91
+ else:
92
+ return render_template("workflow_view.html", workflow=workflow, grouped=grouped)
93
+
94
+
95
+ @data.delete("/executions/records/<int:workflow_id>")
91
96
  @login_required
92
- def delete_workflow_data(workflow_id: int):
97
+ def delete_workflow_record(workflow_id: int):
93
98
  """
94
- .. :quickref: Database; delete experiment data from database
99
+ .. :quickref: Workflow Data Database; delete a workflow execution record
95
100
 
96
- delete workflow data from database
101
+ delete a workflow execution record by workflow id
97
102
 
98
- .. http:get:: /database/workflows/delete/<int:workflow_id>
103
+ .. http:delete:: /executions/records/<int:workflow_id>
99
104
 
100
105
  :param workflow_id: workflow id
101
106
  :type workflow_id: int
102
- :status 302: redirect to :http:get:`/ivoryos/database/workflows/`
103
-
107
+ :status 200: return success message
104
108
  """
105
109
  run = WorkflowRun.query.get(workflow_id)
106
110
  db.session.delete(run)
107
111
  db.session.commit()
108
- return redirect(url_for('database.list_workflows'))
112
+ return jsonify(success=True)
113
+
114
+
115
+ @data.route('/files/execution-data/<string:filename>')
116
+ def download_results(filename:str):
117
+ """
118
+ .. :quickref: Workflow data; download a workflow data file (.CSV)
119
+
120
+ .. http:get:: /files/execution-data/<string:filename>
121
+
122
+ :param filename: workflow data filename
123
+ :type filename: str
124
+
125
+ # :status 302: load pseudo deck and then redirects to :http:get:`/ivoryos/executions`
126
+ """
127
+
128
+ filepath = os.path.join(current_app.config["DATA_FOLDER"], filename)
129
+ return send_file(os.path.abspath(filepath), as_attachment=True)