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.
- ivoryos/__init__.py +12 -5
- ivoryos/routes/api/api.py +5 -58
- ivoryos/routes/control/control.py +46 -43
- ivoryos/routes/control/control_file.py +4 -4
- ivoryos/routes/control/control_new_device.py +10 -10
- ivoryos/routes/control/templates/controllers.html +38 -9
- ivoryos/routes/control/templates/controllers_new.html +1 -1
- ivoryos/routes/data/data.py +81 -60
- ivoryos/routes/data/templates/components/step_card.html +9 -3
- ivoryos/routes/data/templates/workflow_database.html +10 -4
- ivoryos/routes/design/design.py +306 -243
- ivoryos/routes/design/design_file.py +42 -31
- ivoryos/routes/design/design_step.py +132 -30
- ivoryos/routes/design/templates/components/action_form.html +4 -3
- ivoryos/routes/design/templates/components/{instrument_panel.html → actions_panel.html} +7 -5
- ivoryos/routes/design/templates/components/autofill_toggle.html +8 -12
- ivoryos/routes/design/templates/components/canvas.html +5 -14
- ivoryos/routes/design/templates/components/canvas_footer.html +5 -1
- ivoryos/routes/design/templates/components/canvas_header.html +36 -15
- ivoryos/routes/design/templates/components/canvas_main.html +34 -0
- ivoryos/routes/design/templates/components/deck_selector.html +8 -10
- ivoryos/routes/design/templates/components/edit_action_form.html +16 -7
- ivoryos/routes/design/templates/components/instruments_panel.html +66 -0
- ivoryos/routes/design/templates/components/modals/drop_modal.html +3 -5
- ivoryos/routes/design/templates/components/modals/new_script_modal.html +1 -2
- ivoryos/routes/design/templates/components/modals/rename_modal.html +5 -5
- ivoryos/routes/design/templates/components/modals/saveas_modal.html +2 -2
- ivoryos/routes/design/templates/components/python_code_overlay.html +26 -4
- ivoryos/routes/design/templates/components/sidebar.html +12 -13
- ivoryos/routes/design/templates/experiment_builder.html +20 -20
- ivoryos/routes/execute/execute.py +157 -13
- ivoryos/routes/execute/execute_file.py +38 -4
- ivoryos/routes/execute/templates/components/tab_bayesian.html +365 -114
- ivoryos/routes/execute/templates/components/tab_configuration.html +1 -1
- ivoryos/routes/library/library.py +70 -115
- ivoryos/routes/library/templates/library.html +27 -19
- ivoryos/static/js/action_handlers.js +213 -0
- ivoryos/static/js/db_delete.js +23 -0
- ivoryos/static/js/script_metadata.js +39 -0
- ivoryos/static/js/sortable_design.js +89 -56
- ivoryos/static/js/ui_state.js +113 -0
- ivoryos/utils/bo_campaign.py +137 -1
- ivoryos/utils/db_models.py +14 -5
- ivoryos/utils/form.py +4 -9
- ivoryos/utils/global_config.py +13 -1
- ivoryos/utils/script_runner.py +24 -5
- ivoryos/utils/serilize.py +203 -0
- ivoryos/utils/task_runner.py +4 -1
- ivoryos/version.py +1 -1
- {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/METADATA +1 -1
- {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/RECORD +54 -51
- ivoryos/routes/design/templates/components/action_list.html +0 -15
- ivoryos/routes/design/templates/components/operations_panel.html +0 -43
- ivoryos/routes/design/templates/components/script_info.html +0 -31
- ivoryos/routes/design/templates/components/scripts.html +0 -50
- {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/LICENSE +0 -0
- {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.dist-info}/WHEEL +0 -0
- {ivoryos-1.1.0.dist-info → ivoryos-1.2.0.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}/
|
|
46
|
-
app.register_blueprint(design, url_prefix=f'{url_prefix}
|
|
47
|
-
app.register_blueprint(execute, url_prefix=f'{url_prefix}
|
|
48
|
-
app.register_blueprint(data, url_prefix=f'{url_prefix}
|
|
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 =
|
|
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
|
|
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("/
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
flash(f"\nRun Success! Output value: {output}.")
|
|
54
|
-
|
|
55
|
-
flash(
|
|
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>/
|
|
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:: /
|
|
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
|
-
|
|
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;
|
|
107
|
+
.. :quickref: Control Customization; Toggle function visibility
|
|
91
108
|
|
|
92
|
-
.. http:
|
|
109
|
+
.. http:patch:: /instruments/<instrument>/actions/<function>
|
|
93
110
|
|
|
94
|
-
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
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("/
|
|
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:: /
|
|
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("/
|
|
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:: /
|
|
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/
|
|
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("/
|
|
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:: /
|
|
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:
|
|
91
|
+
.. :quickref: Advanced Features; connect to a new device
|
|
92
92
|
|
|
93
93
|
interface for connecting a new <instrument>
|
|
94
94
|
|
|
95
|
-
.. http:get:: /
|
|
95
|
+
.. http:get:: /instruments/new/
|
|
96
96
|
|
|
97
97
|
:param instrument: instrument name
|
|
98
98
|
:type instrument: str
|
|
99
99
|
|
|
100
|
-
.. http:post:: /
|
|
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">{{
|
|
23
|
+
<span class="flex-grow-1">{{ inst | format_name }}</span>
|
|
24
24
|
{% if instrument == inst %}
|
|
25
25
|
<span class="ms-auto">></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">{{
|
|
43
|
+
<span class="flex-grow-1">{{ inst | format_name }}</span>
|
|
44
44
|
{% if instrument == inst %}
|
|
45
45
|
<span class="ms-auto">></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
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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;">
|
|
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 }}
|
|
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>
|
ivoryos/routes/data/data.py
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
|
|
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
|
|
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('/
|
|
12
|
+
@data.route('/executions/records')
|
|
12
13
|
def list_workflows():
|
|
13
14
|
"""
|
|
14
|
-
.. :quickref: Database; list all workflow
|
|
15
|
+
.. :quickref: Workflow Execution Database; list all workflow execution records
|
|
15
16
|
|
|
16
|
-
list all workflow
|
|
17
|
+
list all workflow execution records
|
|
17
18
|
|
|
18
|
-
.. http:get:: /
|
|
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.
|
|
40
|
-
def
|
|
40
|
+
@data.get("/executions/records/<int:workflow_id>")
|
|
41
|
+
def workflow_logs(workflow_id:int):
|
|
41
42
|
"""
|
|
42
|
-
.. :quickref: Database;
|
|
43
|
+
.. :quickref: Workflow Data Database; get workflow data, steps, and logs
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
get workflow data logs by workflow id
|
|
45
46
|
|
|
46
|
-
.. http:get:: /
|
|
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
|
-
|
|
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
|
|
97
|
+
def delete_workflow_record(workflow_id: int):
|
|
93
98
|
"""
|
|
94
|
-
.. :quickref: Database; delete
|
|
99
|
+
.. :quickref: Workflow Data Database; delete a workflow execution record
|
|
95
100
|
|
|
96
|
-
delete workflow
|
|
101
|
+
delete a workflow execution record by workflow id
|
|
97
102
|
|
|
98
|
-
.. http:
|
|
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
|
|
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
|
|
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)
|