ivoryos 1.0.8__py3-none-any.whl → 1.1.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 +19 -7
- ivoryos/routes/api/api.py +109 -0
- ivoryos/routes/auth/auth.py +5 -5
- ivoryos/routes/control/control.py +55 -353
- ivoryos/routes/control/control_file.py +36 -0
- ivoryos/routes/control/control_new_device.py +142 -0
- ivoryos/routes/control/templates/controllers.html +137 -0
- ivoryos/routes/control/templates/controllers_new.html +112 -0
- ivoryos/routes/control/utils.py +38 -0
- ivoryos/routes/data/data.py +108 -0
- ivoryos/routes/{database/templates/database → data/templates}/workflow_database.html +7 -7
- ivoryos/routes/{database/templates/database → data/templates}/workflow_view.html +3 -3
- ivoryos/routes/design/__init__.py +4 -0
- ivoryos/routes/design/design.py +96 -517
- ivoryos/routes/design/design_file.py +57 -0
- ivoryos/routes/design/design_step.py +43 -0
- ivoryos/routes/design/templates/components/action_form.html +52 -0
- ivoryos/routes/design/templates/components/action_list.html +15 -0
- ivoryos/routes/design/templates/components/autofill_toggle.html +14 -0
- ivoryos/routes/design/templates/components/canvas.html +14 -0
- ivoryos/routes/design/templates/components/canvas_footer.html +5 -0
- ivoryos/routes/design/templates/components/canvas_header.html +54 -0
- ivoryos/routes/design/templates/components/deck_selector.html +12 -0
- ivoryos/routes/design/templates/components/edit_action_form.html +29 -0
- ivoryos/routes/design/templates/components/instrument_panel.html +23 -0
- ivoryos/routes/design/templates/components/modals/drop_modal.html +19 -0
- ivoryos/routes/design/templates/components/modals/json_modal.html +22 -0
- ivoryos/routes/design/templates/components/modals/new_script_modal.html +18 -0
- ivoryos/routes/design/templates/components/modals/rename_modal.html +23 -0
- ivoryos/routes/design/templates/components/modals/saveas_modal.html +27 -0
- ivoryos/routes/design/templates/components/modals.html +6 -0
- ivoryos/routes/design/templates/components/operations_panel.html +43 -0
- ivoryos/routes/design/templates/components/python_code_overlay.html +17 -0
- ivoryos/routes/design/templates/components/script_info.html +31 -0
- ivoryos/routes/design/templates/components/scripts.html +50 -0
- ivoryos/routes/design/templates/components/sidebar.html +16 -0
- ivoryos/routes/design/templates/components/text_to_code_panel.html +20 -0
- ivoryos/routes/design/templates/experiment_builder.html +41 -0
- ivoryos/routes/execute/__init__.py +0 -0
- ivoryos/routes/execute/execute.py +173 -0
- ivoryos/routes/execute/execute_file.py +44 -0
- ivoryos/routes/execute/templates/components/error_modal.html +20 -0
- ivoryos/routes/execute/templates/components/logging_panel.html +31 -0
- ivoryos/routes/execute/templates/components/progress_panel.html +27 -0
- ivoryos/routes/execute/templates/components/run_panel.html +9 -0
- ivoryos/routes/execute/templates/components/run_tabs.html +17 -0
- ivoryos/routes/execute/templates/components/tab_bayesian.html +147 -0
- ivoryos/routes/execute/templates/components/tab_configuration.html +98 -0
- ivoryos/routes/execute/templates/components/tab_repeat.html +14 -0
- ivoryos/routes/execute/templates/experiment_run.html +294 -0
- ivoryos/routes/library/__init__.py +0 -0
- ivoryos/routes/{database/database.py → library/library.py} +10 -112
- ivoryos/routes/{database/templates/database/scripts_database.html → library/templates/library.html} +8 -8
- ivoryos/routes/main/main.py +1 -1
- ivoryos/routes/main/templates/{main/home.html → home.html} +4 -4
- ivoryos/socket_handlers.py +52 -0
- ivoryos/templates/base.html +4 -4
- ivoryos/utils/bo_campaign.py +43 -3
- ivoryos/utils/form.py +1 -0
- ivoryos/utils/py_to_json.py +225 -0
- ivoryos/utils/script_runner.py +30 -7
- ivoryos/version.py +1 -1
- {ivoryos-1.0.8.dist-info → ivoryos-1.1.0.dist-info}/METADATA +5 -8
- ivoryos-1.1.0.dist-info/RECORD +102 -0
- ivoryos/routes/control/templates/control/controllers.html +0 -78
- ivoryos/routes/control/templates/control/controllers_home.html +0 -55
- ivoryos/routes/control/templates/control/controllers_new.html +0 -89
- ivoryos/routes/design/templates/design/experiment_builder.html +0 -521
- ivoryos/routes/design/templates/design/experiment_run.html +0 -558
- ivoryos-1.0.8.dist-info/RECORD +0 -61
- /ivoryos/routes/auth/templates/{auth/login.html → login.html} +0 -0
- /ivoryos/routes/auth/templates/{auth/signup.html → signup.html} +0 -0
- /ivoryos/routes/{database → data}/__init__.py +0 -0
- /ivoryos/routes/{database/templates/database → data/templates/components}/step_card.html +0 -0
- /ivoryos/routes/main/templates/{main/help.html → help.html} +0 -0
- {ivoryos-1.0.8.dist-info → ivoryos-1.1.0.dist-info}/LICENSE +0 -0
- {ivoryos-1.0.8.dist-info → ivoryos-1.1.0.dist-info}/WHEEL +0 -0
- {ivoryos-1.0.8.dist-info → ivoryos-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from flask import Blueprint, request,current_app, send_file
|
|
3
|
+
from flask_login import login_required
|
|
4
|
+
|
|
5
|
+
from ivoryos.utils.client_proxy import export_to_python, create_function
|
|
6
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
7
|
+
|
|
8
|
+
global_config = GlobalConfig()
|
|
9
|
+
|
|
10
|
+
control_file = Blueprint('file', __name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@control_file.route("/download/proxy", strict_slashes=False)
|
|
14
|
+
@login_required
|
|
15
|
+
def download_proxy():
|
|
16
|
+
"""
|
|
17
|
+
.. :quickref: Direct Control; download proxy interface
|
|
18
|
+
|
|
19
|
+
download proxy interface
|
|
20
|
+
|
|
21
|
+
.. http:get:: /control/download
|
|
22
|
+
"""
|
|
23
|
+
snapshot = global_config.deck_snapshot.copy()
|
|
24
|
+
class_definitions = {}
|
|
25
|
+
# Iterate through each instrument in the snapshot
|
|
26
|
+
for instrument_key, instrument_data in snapshot.items():
|
|
27
|
+
# Iterate through each function associated with the current instrument
|
|
28
|
+
for function_key, function_data in instrument_data.items():
|
|
29
|
+
# Convert the function signature to a string representation
|
|
30
|
+
function_data['signature'] = str(function_data['signature'])
|
|
31
|
+
class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
|
|
32
|
+
class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
|
|
33
|
+
# Export the generated class definitions to a .py script
|
|
34
|
+
export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
|
|
35
|
+
filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
|
|
36
|
+
return send_file(os.path.abspath(filepath), as_attachment=True)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from flask import Blueprint, request, current_app, send_file, flash, redirect, url_for, session, render_template
|
|
3
|
+
from flask_login import login_required
|
|
4
|
+
|
|
5
|
+
from ivoryos.utils import utils
|
|
6
|
+
from ivoryos.routes.control.utils import find_instrument_by_name
|
|
7
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
8
|
+
|
|
9
|
+
global_config = GlobalConfig()
|
|
10
|
+
|
|
11
|
+
control_temp = Blueprint('temp', __name__)
|
|
12
|
+
|
|
13
|
+
@control_temp.route("/import/module", methods=['POST'])
|
|
14
|
+
def import_api():
|
|
15
|
+
"""
|
|
16
|
+
.. :quickref: Advanced Features; Manually import API module(s)
|
|
17
|
+
|
|
18
|
+
importing other Python modules
|
|
19
|
+
|
|
20
|
+
.. http:post:: /control/import/module
|
|
21
|
+
|
|
22
|
+
:form filepath: API (Python class) module filepath
|
|
23
|
+
|
|
24
|
+
import the module and redirect to :http:get:`/ivoryos/control/new/`
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
filepath = request.form.get('filepath')
|
|
28
|
+
# filepath.replace('\\', '/')
|
|
29
|
+
name = os.path.split(filepath)[-1].split('.')[0]
|
|
30
|
+
try:
|
|
31
|
+
spec = utils.importlib.util.spec_from_file_location(name, filepath)
|
|
32
|
+
module = utils.importlib.util.module_from_spec(spec)
|
|
33
|
+
spec.loader.exec_module(module)
|
|
34
|
+
classes = utils.inspect.getmembers(module, utils.inspect.isclass)
|
|
35
|
+
if len(classes) == 0:
|
|
36
|
+
flash("Invalid import: no class found in the path")
|
|
37
|
+
return redirect(url_for("control.controllers_home"))
|
|
38
|
+
for i in classes:
|
|
39
|
+
globals()[i[0]] = i[1]
|
|
40
|
+
global_config.api_variables.add(i[0])
|
|
41
|
+
# should handle path error and file type error
|
|
42
|
+
except Exception as e:
|
|
43
|
+
flash(e.__str__())
|
|
44
|
+
return redirect(url_for("control.temp.new_controller"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@control_temp.route("/import/deck", methods=['POST'])
|
|
49
|
+
def import_deck():
|
|
50
|
+
"""
|
|
51
|
+
.. :quickref: Advanced Features; Manually import a deck
|
|
52
|
+
|
|
53
|
+
.. http:post:: /control/import_deck
|
|
54
|
+
|
|
55
|
+
:form filepath: deck module filepath
|
|
56
|
+
|
|
57
|
+
import the module and redirect to the previous page
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
script = utils.get_script_file()
|
|
61
|
+
filepath = request.form.get('filepath')
|
|
62
|
+
session['dismiss'] = request.form.get('dismiss')
|
|
63
|
+
update = request.form.get('update')
|
|
64
|
+
back = request.referrer
|
|
65
|
+
if session['dismiss']:
|
|
66
|
+
return redirect(back)
|
|
67
|
+
name = os.path.split(filepath)[-1].split('.')[0]
|
|
68
|
+
try:
|
|
69
|
+
module = utils.import_module_by_filepath(filepath=filepath, name=name)
|
|
70
|
+
utils.save_to_history(filepath, current_app.config["DECK_HISTORY"])
|
|
71
|
+
module_sigs = utils.create_deck_snapshot(module, save=update, output_path=current_app.config["DUMMY_DECK"])
|
|
72
|
+
if not len(module_sigs) > 0:
|
|
73
|
+
flash("Invalid hardware deck, connect instruments in deck script", "error")
|
|
74
|
+
return redirect(url_for("control.deck_controllers"))
|
|
75
|
+
global_config.deck = module
|
|
76
|
+
global_config.deck_snapshot = module_sigs
|
|
77
|
+
|
|
78
|
+
if script.deck is None:
|
|
79
|
+
script.deck = module.__name__
|
|
80
|
+
# file path error exception
|
|
81
|
+
except Exception as e:
|
|
82
|
+
flash(e.__str__())
|
|
83
|
+
return redirect(back)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@control_temp.route("/new/", strict_slashes=False)
|
|
87
|
+
@control_temp.route("/new/<instrument>", methods=['GET', 'POST'])
|
|
88
|
+
@login_required
|
|
89
|
+
def new_controller(instrument=None):
|
|
90
|
+
"""
|
|
91
|
+
.. :quickref: Direct Control; connect to a new device
|
|
92
|
+
|
|
93
|
+
interface for connecting a new <instrument>
|
|
94
|
+
|
|
95
|
+
.. http:get:: /control/new/
|
|
96
|
+
|
|
97
|
+
:param instrument: instrument name
|
|
98
|
+
:type instrument: str
|
|
99
|
+
|
|
100
|
+
.. http:post:: /control/new/
|
|
101
|
+
|
|
102
|
+
:form device_name: module instance name (e.g. my_instance = MyClass())
|
|
103
|
+
:form kwargs: dynamic module initialization kwargs fields
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
device = None
|
|
107
|
+
args = None
|
|
108
|
+
if instrument:
|
|
109
|
+
|
|
110
|
+
device = globals()[instrument]
|
|
111
|
+
args = utils.inspect.signature(device.__init__)
|
|
112
|
+
|
|
113
|
+
if request.method == 'POST':
|
|
114
|
+
device_name = request.form.get("device_name", "")
|
|
115
|
+
if device_name and device_name in globals():
|
|
116
|
+
flash("Device name is defined. Try another name, or leave it as blank to auto-configure")
|
|
117
|
+
# return render_template('controllers_new.html', instrument=instrument,
|
|
118
|
+
# api_variables=global_config.api_variables,
|
|
119
|
+
# device=device, args=args, defined_variables=global_config.defined_variables)
|
|
120
|
+
if device_name == "":
|
|
121
|
+
device_name = device.__name__.lower() + "_"
|
|
122
|
+
num = 1
|
|
123
|
+
while device_name + str(num) in global_config.defined_variables:
|
|
124
|
+
num += 1
|
|
125
|
+
device_name = device_name + str(num)
|
|
126
|
+
kwargs = request.form.to_dict()
|
|
127
|
+
kwargs.pop("device_name")
|
|
128
|
+
for i in kwargs:
|
|
129
|
+
if kwargs[i] in global_config.defined_variables:
|
|
130
|
+
kwargs[i] = global_config.defined_variables[kwargs[i]]
|
|
131
|
+
try:
|
|
132
|
+
utils.convert_config_type(kwargs, device.__init__.__annotations__, is_class=True)
|
|
133
|
+
except Exception as e:
|
|
134
|
+
flash(e)
|
|
135
|
+
try:
|
|
136
|
+
global_config.defined_variables[device_name] = device(**kwargs)
|
|
137
|
+
# global_config.defined_variables.add(device_name)
|
|
138
|
+
return redirect(url_for('control.deck_controllers'))
|
|
139
|
+
except Exception as e:
|
|
140
|
+
flash(e)
|
|
141
|
+
return render_template('controllers_new.html', instrument=instrument, api_variables=global_config.api_variables,
|
|
142
|
+
device=device, args=args, defined_variables=global_config.defined_variables)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
{% block title %}IvoryOS | Controllers {% endblock %}
|
|
3
|
+
|
|
4
|
+
{% block body %}
|
|
5
|
+
<div id="overlay" class="overlay">
|
|
6
|
+
<div>
|
|
7
|
+
<h3 id="overlay-text"></h3>
|
|
8
|
+
<div class="spinner-border" role="status"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="container-fluid">
|
|
12
|
+
<div class="row">
|
|
13
|
+
<!-- Sidebar: Instruments -->
|
|
14
|
+
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar py-3" style="height: 100vh; overflow-y: auto; position: sticky; top: 0;">
|
|
15
|
+
<div class="sidebar-sticky">
|
|
16
|
+
<!-- Deck Instruments -->
|
|
17
|
+
<div class="mb-4">
|
|
18
|
+
<div class="list-group">
|
|
19
|
+
{% for inst in defined_variables %}
|
|
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
|
+
href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
|
|
22
|
+
style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
|
|
23
|
+
<span class="flex-grow-1">{{ format_name(inst) }}</span>
|
|
24
|
+
{% if instrument == inst %}
|
|
25
|
+
<span class="ms-auto">></span>
|
|
26
|
+
{% endif %}
|
|
27
|
+
</a>
|
|
28
|
+
{% endfor %}
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
<!-- Temp Instruments -->
|
|
35
|
+
{% if temp_variables %}
|
|
36
|
+
<div class="mb-4">
|
|
37
|
+
<h6 class="fw-bold text-secondary mb-2" style="letter-spacing: 1px;">Temp Instruments</h6>
|
|
38
|
+
<div class="list-group">
|
|
39
|
+
{% for inst in temp_variables %}
|
|
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
|
+
href="{{ url_for('control.deck_controllers') }}?instrument={{ inst }}"
|
|
42
|
+
style="border-radius: 0.5rem; margin-bottom: 0.5rem; transition: background 0.2s;">
|
|
43
|
+
<span class="flex-grow-1">{{ format_name(inst) }}</span>
|
|
44
|
+
{% if instrument == inst %}
|
|
45
|
+
<span class="ms-auto">></span>
|
|
46
|
+
{% endif %}
|
|
47
|
+
</a>
|
|
48
|
+
{% endfor %}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
{% endif %}
|
|
52
|
+
<!-- Action Buttons -->
|
|
53
|
+
<div class="mb-4">
|
|
54
|
+
<a href="{{ url_for('control.file.download_proxy', filetype='proxy') }}" class="btn btn-outline-primary w-100 mb-2">
|
|
55
|
+
Download proxy
|
|
56
|
+
</a>
|
|
57
|
+
<a href="{{ url_for('control.temp.new_controller') }}" class="btn btn-outline-success w-100">
|
|
58
|
+
New connection
|
|
59
|
+
</a>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</nav>
|
|
63
|
+
|
|
64
|
+
<!-- Main: Method Cards -->
|
|
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 %}
|
|
67
|
+
{# <h2 class="text-secondary">{{ instrument }} controller</h2>#}
|
|
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
|
+
{% set hidden = session.get('hidden_functions', {}) %}
|
|
70
|
+
{% set hidden_instrument = hidden.get(instrument, []) %}
|
|
71
|
+
{% for function, form in forms.items() %}
|
|
72
|
+
{% if function not in hidden_instrument %}
|
|
73
|
+
<div class="card" id="{{function}}" style="margin: 0;">
|
|
74
|
+
<div class="bg-white rounded shadow-sm h-100">
|
|
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 }}">
|
|
79
|
+
<div class="form-group">
|
|
80
|
+
{{ form.hidden_tag() }}
|
|
81
|
+
{% for field in form %}
|
|
82
|
+
{% if field.type not in ['CSRFTokenField', 'HiddenField'] %}
|
|
83
|
+
<div class="input-group mb-3">
|
|
84
|
+
<label class="input-group-text">{{ field.label.text }}</label>
|
|
85
|
+
{% if field.type == "SubmitField" %}
|
|
86
|
+
{{ field(class="btn btn-dark") }}
|
|
87
|
+
{% elif field.type == "BooleanField" %}
|
|
88
|
+
{{ field(class="form-check-input") }}
|
|
89
|
+
{% else %}
|
|
90
|
+
{{ field(class="form-control") }}
|
|
91
|
+
{% endif %}
|
|
92
|
+
</div>
|
|
93
|
+
{% endif %}
|
|
94
|
+
{% endfor %}
|
|
95
|
+
</div>
|
|
96
|
+
<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>
|
|
98
|
+
</div>
|
|
99
|
+
</form>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
{% endif %}
|
|
104
|
+
{% endfor %}
|
|
105
|
+
</div>
|
|
106
|
+
<!-- Hidden functions accordion -->
|
|
107
|
+
<div class="accordion accordion-flush" id="accordionActions" >
|
|
108
|
+
<div class="accordion-item">
|
|
109
|
+
<h4 class="accordion-header">
|
|
110
|
+
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#hidden">
|
|
111
|
+
Hidden functions
|
|
112
|
+
</button>
|
|
113
|
+
</h4>
|
|
114
|
+
</div>
|
|
115
|
+
<div id="hidden" class="accordion-collapse collapse" data-bs-parent="#accordionActions">
|
|
116
|
+
<div class="accordion-body">
|
|
117
|
+
{% for function in hidden_instrument %}
|
|
118
|
+
<div>
|
|
119
|
+
{{ function }} <a href="{{ url_for('control.remove_hidden', instrument=instrument, function=function) }}"><i class="bi bi-eye-fill"></i></a>
|
|
120
|
+
</div>
|
|
121
|
+
{% endfor %}
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<script>
|
|
126
|
+
const saveOrderUrl = `{{ url_for('control.save_order', instrument=instrument) }}`;
|
|
127
|
+
const buttonIds = {{ session['card_order'][instrument] | tojson }};
|
|
128
|
+
</script>
|
|
129
|
+
<script src="{{ url_for('static', filename='js/sortable_card.js') }}"></script>
|
|
130
|
+
<script src="{{ url_for('static', filename='js/overlay.js') }}"></script>
|
|
131
|
+
{% else %}
|
|
132
|
+
<div class="alert alert-info mt-4">Select an instrument to view its methods.</div>
|
|
133
|
+
{% endif %}
|
|
134
|
+
</main>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
{% endblock %}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
{% block title %}IvoryOS | New devices{% endblock %}
|
|
3
|
+
|
|
4
|
+
{% block body %}
|
|
5
|
+
<div class="row">
|
|
6
|
+
<!-- Available Python API -->
|
|
7
|
+
<div class="col-xl-4 col-lg-4 col-md-6 mb-4">
|
|
8
|
+
<div class="card shadow-sm mb-4">
|
|
9
|
+
<div class="card-header">
|
|
10
|
+
<h5 class="mb-0">Available Python API</h5>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card-body">
|
|
13
|
+
{% for instrument in api_variables %}
|
|
14
|
+
<div class="card mb-2">
|
|
15
|
+
<div class="card-body p-2">
|
|
16
|
+
<a href="{{ url_for('control.temp.new_controller', instrument=instrument) }}" class="text-dark stretched-link">{{ instrument }}</a>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
{% endfor %}
|
|
20
|
+
<div class="card mt-3">
|
|
21
|
+
<div class="card-body p-2">
|
|
22
|
+
<a data-bs-toggle="modal" href="#importAPI" class="stretched-link">
|
|
23
|
+
<i class="bi bi-folder-plus"></i> Import API
|
|
24
|
+
</a>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Connecting Device -->
|
|
32
|
+
<div class="col-xl-5 col-lg-5 col-md-6 mb-4">
|
|
33
|
+
{% if device %}
|
|
34
|
+
{{ device }}
|
|
35
|
+
<div class="card shadow-sm mb-4">
|
|
36
|
+
<div class="card-header">
|
|
37
|
+
<h5 class="mb-0">Connecting</h5>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="card-body">
|
|
40
|
+
<form role="form" method="POST" name="init" action="{{ url_for('control.temp.new_controller', instrument=instrument) }}">
|
|
41
|
+
<div class="mb-3">
|
|
42
|
+
<label class="form-label" for="device_name">Name this device</label>
|
|
43
|
+
<input class="form-control" type="text" id="device_name" name="device_name" aria-describedby="nameHelpBlock" placeholder="e.g. {{device.__name__}}_1">
|
|
44
|
+
<div id="nameHelpBlock" class="form-text">
|
|
45
|
+
Name your instrument, avoid names that are defined on the right
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
{% for arg in device.__init__.__annotations__ %}
|
|
49
|
+
{% if not arg == "return" %}
|
|
50
|
+
<div class="mb-3">
|
|
51
|
+
<label class="form-label" for="{{arg}}">{{arg}}</label>
|
|
52
|
+
<input class="form-control" type="text" id="{{arg}}" name="{{arg}}"
|
|
53
|
+
placeholder="{{device.__init__.__annotations__[arg].__name__}}"
|
|
54
|
+
value="{{args.parameters[arg].default if not args.parameters[arg].default.__name__ == '_empty' else ''}}">
|
|
55
|
+
{% if device.__init__.__annotations__[arg].__module__ is not in ["builtins", "typing"] %}
|
|
56
|
+
<a role="button" href="{{ url_for('control.temp.new_controller', instrument=device.__init__.__annotations__[arg].__name__) }}" class="btn btn-secondary btn-sm mt-2">Initialize {{device.__init__.__annotations__[arg].__name__}} first</a>
|
|
57
|
+
{% endif %}
|
|
58
|
+
</div>
|
|
59
|
+
{% endif %}
|
|
60
|
+
{% endfor %}
|
|
61
|
+
<button type="submit" class="btn btn-dark">Connect</button>
|
|
62
|
+
</form>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{% endif %}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Defined Instruments -->
|
|
69
|
+
<div class="col-xl-3 col-lg-3 col-md-6 mb-4">
|
|
70
|
+
<div class="card shadow-sm mb-4">
|
|
71
|
+
<div class="card-header">
|
|
72
|
+
<h5 class="mb-0">Defined Instruments</h5>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="card-body">
|
|
75
|
+
{% if defined_variables %}
|
|
76
|
+
<ul class="list-group">
|
|
77
|
+
{% for instrument in defined_variables %}
|
|
78
|
+
<li class="list-group-item">{{ instrument }}</li>
|
|
79
|
+
{% endfor %}
|
|
80
|
+
</ul>
|
|
81
|
+
{% else %}
|
|
82
|
+
<span class="text-muted">No instruments defined.</span>
|
|
83
|
+
{% endif %}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<!-- Import API Modal -->
|
|
90
|
+
<div class="modal fade" id="importAPI" tabindex="-1" aria-labelledby="importModal" aria-hidden="true">
|
|
91
|
+
<div class="modal-dialog">
|
|
92
|
+
<div class="modal-content">
|
|
93
|
+
<div class="modal-header">
|
|
94
|
+
<h1 class="modal-title fs-5" id="importModal">Import API by file path</h1>
|
|
95
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
96
|
+
</div>
|
|
97
|
+
<form method="POST" action="{{ url_for('control.temp.import_api') }}" enctype="multipart/form-data">
|
|
98
|
+
<div class="modal-body">
|
|
99
|
+
<div class="mb-3">
|
|
100
|
+
<label class="form-label" for="filepath">File Path:</label>
|
|
101
|
+
<input type="text" class="form-control" name="filepath" id="filepath">
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="modal-footer">
|
|
105
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
106
|
+
<button type="submit" class="btn btn-primary">Save</button>
|
|
107
|
+
</div>
|
|
108
|
+
</form>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
{% endblock %}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from flask import session
|
|
2
|
+
|
|
3
|
+
from ivoryos.utils.global_config import GlobalConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
global_config = GlobalConfig()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def find_instrument_by_name(name: str):
|
|
10
|
+
"""
|
|
11
|
+
find instrument class object by instance name
|
|
12
|
+
"""
|
|
13
|
+
if name.startswith("deck"):
|
|
14
|
+
name = name.replace("deck.", "")
|
|
15
|
+
return getattr(global_config.deck, name)
|
|
16
|
+
elif name in global_config.defined_variables:
|
|
17
|
+
return global_config.defined_variables[name]
|
|
18
|
+
elif name in globals():
|
|
19
|
+
return globals()[name]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_session_by_instrument(session_name, instrument):
|
|
23
|
+
"""get data from session by instrument"""
|
|
24
|
+
session_object = session.get(session_name, {})
|
|
25
|
+
functions = session_object.get(instrument, [])
|
|
26
|
+
return functions
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def post_session_by_instrument(session_name, instrument, data):
|
|
30
|
+
"""
|
|
31
|
+
save new data to session by instrument
|
|
32
|
+
:param session_name: "card_order" or "hidden_functions"
|
|
33
|
+
:param instrument: function name of class object
|
|
34
|
+
:param data: order list or hidden function list
|
|
35
|
+
"""
|
|
36
|
+
session_object = session.get(session_name, {})
|
|
37
|
+
session_object[instrument] = data
|
|
38
|
+
session[session_name] = session_object
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
|
|
2
|
+
from flask_login import login_required
|
|
3
|
+
|
|
4
|
+
from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
|
|
5
|
+
from ivoryos.utils.utils import get_script_file, post_script_file
|
|
6
|
+
|
|
7
|
+
data = Blueprint('data', __name__, template_folder='templates')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@data.route('/all')
|
|
12
|
+
def list_workflows():
|
|
13
|
+
"""
|
|
14
|
+
.. :quickref: Database; list all workflow logs
|
|
15
|
+
|
|
16
|
+
list all workflow logs
|
|
17
|
+
|
|
18
|
+
.. http:get:: /database/workflows/
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
query = WorkflowRun.query.order_by(WorkflowRun.id.desc())
|
|
22
|
+
search_term = request.args.get("keyword", None)
|
|
23
|
+
if search_term:
|
|
24
|
+
query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
|
|
25
|
+
page = request.args.get('page', default=1, type=int)
|
|
26
|
+
per_page = 10
|
|
27
|
+
|
|
28
|
+
workflows = query.paginate(page=page, per_page=per_page, error_out=False)
|
|
29
|
+
if request.accept_mimetypes.best_match(['application/json', 'text/html']) == 'application/json':
|
|
30
|
+
workflows = query.all()
|
|
31
|
+
workflow_data = {w.id:{"workflow_name":w.name, "start_time":w.start_time} for w in workflows}
|
|
32
|
+
return jsonify({
|
|
33
|
+
"workflow_data": workflow_data,
|
|
34
|
+
})
|
|
35
|
+
else:
|
|
36
|
+
return render_template('workflow_database.html', workflows=workflows)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@data.route("/get/<int:workflow_id>")
|
|
40
|
+
def get_workflow_steps(workflow_id:int):
|
|
41
|
+
"""
|
|
42
|
+
.. :quickref: Database; list all workflow logs
|
|
43
|
+
|
|
44
|
+
list all workflow logs
|
|
45
|
+
|
|
46
|
+
.. http:get:: /database/workflows/<int:workflow_id>
|
|
47
|
+
|
|
48
|
+
"""
|
|
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
|
+
|
|
90
|
+
@data.route("/delete/<int:workflow_id>")
|
|
91
|
+
@login_required
|
|
92
|
+
def delete_workflow_data(workflow_id: int):
|
|
93
|
+
"""
|
|
94
|
+
.. :quickref: Database; delete experiment data from database
|
|
95
|
+
|
|
96
|
+
delete workflow data from database
|
|
97
|
+
|
|
98
|
+
.. http:get:: /database/workflows/delete/<int:workflow_id>
|
|
99
|
+
|
|
100
|
+
:param workflow_id: workflow id
|
|
101
|
+
:type workflow_id: int
|
|
102
|
+
:status 302: redirect to :http:get:`/ivoryos/database/workflows/`
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
run = WorkflowRun.query.get(workflow_id)
|
|
106
|
+
db.session.delete(run)
|
|
107
|
+
db.session.commit()
|
|
108
|
+
return redirect(url_for('database.list_workflows'))
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
{% block title %}IvoryOS | Design Database{% endblock %}
|
|
4
4
|
{% block body %}
|
|
5
5
|
<div class="div">
|
|
6
|
-
<form id="search" style="display: inline-block;float: right;" action="{{url_for('
|
|
6
|
+
<form id="search" style="display: inline-block;float: right;" action="{{url_for('data.list_workflows',deck_name=deck_name)}}" method="GET">
|
|
7
7
|
<div class="input-group">
|
|
8
8
|
<div class="form-outline">
|
|
9
9
|
<input type="search" name="keyword" id="keyword" class="form-control" placeholder="Search workflows...">
|
|
@@ -28,19 +28,19 @@
|
|
|
28
28
|
<tbody>
|
|
29
29
|
{% for workflow in workflows %}
|
|
30
30
|
<tr>
|
|
31
|
-
<td><a href="{{ url_for('
|
|
31
|
+
<td><a href="{{ url_for('data.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
|
|
32
32
|
<td>{{ workflow.id }}</td>
|
|
33
33
|
<td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
|
|
34
34
|
<td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
|
|
35
35
|
|
|
36
36
|
<td>
|
|
37
37
|
{% if workflow.data_path %}
|
|
38
|
-
<a href="{{ url_for('design.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
|
|
38
|
+
<a href="{{ url_for('design.design_files.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
|
|
39
39
|
{% endif %}
|
|
40
40
|
</td>
|
|
41
41
|
<td>
|
|
42
42
|
{% if session['user'] == 'admin' or session['user'] == workflow.author %}
|
|
43
|
-
<a href="{{ url_for('
|
|
43
|
+
<a href="{{ url_for('data.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
|
|
44
44
|
{% else %}
|
|
45
45
|
<a class="disabled-link">delete</a>
|
|
46
46
|
{% endif %}
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
{# paging#}
|
|
54
54
|
<div class="pagination justify-content-center">
|
|
55
55
|
<div class="page-item {{ 'disabled' if not workflows.has_prev else '' }}">
|
|
56
|
-
<a class="page-link" href="{{ url_for('
|
|
56
|
+
<a class="page-link" href="{{ url_for('data.list_workflows', page=workflows.prev_num) }}">Previous</a>
|
|
57
57
|
</div>
|
|
58
58
|
|
|
59
59
|
{% for num in workflows.iter_pages() %}
|
|
60
60
|
{% if num %}
|
|
61
61
|
<div class="page-item {{ 'active' if num == workflows.page else '' }}">
|
|
62
|
-
<a class="page-link" href="{{ url_for('
|
|
62
|
+
<a class="page-link" href="{{ url_for('data.list_workflows', page=num) }}">{{ num }}</a>
|
|
63
63
|
</div>
|
|
64
64
|
{% else %}
|
|
65
65
|
<div class="page-item disabled">
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
{% endfor %}
|
|
70
70
|
|
|
71
71
|
<div class="page-item {{ 'disabled' if not workflows.has_next else '' }}">
|
|
72
|
-
<a class="page-link" href="{{ url_for('
|
|
72
|
+
<a class="page-link" href="{{ url_for('data.list_workflows', page=workflows.next_num) }}">Next</a>
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
75
75
|
|
|
@@ -110,21 +110,21 @@
|
|
|
110
110
|
{% if grouped.prep %}
|
|
111
111
|
<h4 class="mt-4">Prep Phase</h4>
|
|
112
112
|
{% for step in grouped.prep %}
|
|
113
|
-
{% include "step_card.html" %}
|
|
113
|
+
{% include "components/step_card.html" %}
|
|
114
114
|
{% endfor %}
|
|
115
115
|
{% endif %}
|
|
116
116
|
|
|
117
117
|
{% for repeat_index, step_list in grouped.script.items()|sort %}
|
|
118
118
|
<h4 class="mt-4" id="card-iter{{ repeat_index }}">Iteration {{ repeat_index }}</h4>
|
|
119
119
|
{% for step in step_list %}
|
|
120
|
-
{% include "step_card.html" %}
|
|
120
|
+
{% include "components/step_card.html" %}
|
|
121
121
|
{% endfor %}
|
|
122
122
|
{% endfor %}
|
|
123
123
|
|
|
124
124
|
{% if grouped.cleanup %}
|
|
125
125
|
<h4 class="mt-4">Cleanup Phase</h4>
|
|
126
126
|
{% for step in grouped.cleanup %}
|
|
127
|
-
{% include "step_card.html" %}
|
|
127
|
+
{% include "components/step_card.html" %}
|
|
128
128
|
{% endfor %}
|
|
129
129
|
{% endif %}
|
|
130
130
|
{% endblock %}
|