ivoryos 0.1.20__py3-none-any.whl → 0.1.22__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 CHANGED
@@ -1,10 +1,8 @@
1
- import importlib
2
- import inspect
3
1
  import os
4
2
  import sys
5
3
  from typing import Union
6
4
 
7
- from flask import Flask, redirect, url_for, Blueprint, g
5
+ from flask import Flask, redirect, url_for, g
8
6
 
9
7
  from ivoryos.config import Config, get_config
10
8
  from ivoryos.routes.auth.auth import auth, login_manager
@@ -20,14 +18,30 @@ from ivoryos.utils.script_runner import ScriptRunner
20
18
  from ivoryos.version import __version__ as ivoryos_version
21
19
  from importlib.metadata import entry_points
22
20
  global_config = GlobalConfig()
21
+ from sqlalchemy import event
22
+ from sqlalchemy.engine import Engine
23
+ import sqlite3
24
+
25
+ @event.listens_for(Engine, "connect")
26
+ def enforce_sqlite_foreign_keys(dbapi_connection, connection_record):
27
+ if isinstance(dbapi_connection, sqlite3.Connection):
28
+ cursor = dbapi_connection.cursor()
29
+ cursor.execute("PRAGMA foreign_keys=ON")
30
+ cursor.close()
23
31
 
24
32
  url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
25
33
  app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
34
+ app.register_blueprint(main, url_prefix=url_prefix)
35
+ app.register_blueprint(auth, url_prefix=url_prefix)
36
+ app.register_blueprint(control, url_prefix=url_prefix)
37
+ app.register_blueprint(design, url_prefix=url_prefix)
38
+ app.register_blueprint(database, url_prefix=url_prefix)
26
39
 
27
40
 
28
41
  def create_app(config_class=None):
29
- # url_prefix = os.getenv('URL_PREFIX', "/ivoryos")
30
- # app = Flask(__name__, static_url_path=f'{url_prefix}/static', static_folder='static')
42
+ """
43
+ create app, init database
44
+ """
31
45
  app.config.from_object(config_class or 'config.get_config()')
32
46
 
33
47
  # Initialize extensions
@@ -82,18 +96,9 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
82
96
  :param logger: logger name of list of logger names, defaults to None
83
97
  :param logger_output_name: log file save name of logger, defaults to None, and will use "default.log"
84
98
  :param enable_design: enable design canvas, database and workflow execution
85
- :param stream_address:
86
99
  """
87
100
  app = create_app(config_class=config or get_config()) # Create app instance using factory function
88
101
 
89
- app.register_blueprint(main, url_prefix=url_prefix)
90
- app.register_blueprint(auth, url_prefix=url_prefix)
91
- app.register_blueprint(control, url_prefix=url_prefix)
92
-
93
- if enable_design:
94
- app.register_blueprint(design, url_prefix=url_prefix)
95
- app.register_blueprint(database, url_prefix=url_prefix)
96
-
97
102
  plugins = load_plugins(app, socketio)
98
103
 
99
104
  def inject_nav_config():
@@ -115,10 +120,8 @@ def run(module=None, host="0.0.0.0", port=None, debug=None, llm_server=None, mod
115
120
  app.config["MODULE"] = module
116
121
  app.config["OFF_LINE"] = False
117
122
  global_config.deck = sys.modules[module]
118
- # global_config.heinsight = HeinsightAPI("http://127.0.0.1:8080")
119
123
  global_config.deck_snapshot = utils.create_deck_snapshot(global_config.deck,
120
124
  output_path=app.config["DUMMY_DECK"], save=True)
121
- # global_config.runner = ScriptRunner(globals())
122
125
  else:
123
126
  app.config["OFF_LINE"] = True
124
127
  if model:
@@ -156,3 +159,5 @@ def load_plugins(app, socketio):
156
159
  app.register_blueprint(getattr(plugin, entry_point.name), url_prefix=f"{url_prefix}/{entry_point.name}")
157
160
 
158
161
  return plugin_names
162
+
163
+
@@ -151,7 +151,7 @@ def controllers(instrument: str):
151
151
  return render_template('controllers.html', instrument=instrument, forms=forms, format_name=format_name)
152
152
 
153
153
 
154
- @control.route("/backend_control/<instrument>", methods=['GET', 'POST'])
154
+ @control.route("/backend_control/<instrument>", methods=['POST'])
155
155
  def backend_control(instrument: str=None):
156
156
  """
157
157
  .. :quickref: Backend Control; backend control
@@ -187,7 +187,7 @@ def backend_control(instrument: str=None):
187
187
  return json_output, 400
188
188
  else:
189
189
  return "instrument not exist", 400
190
- return json_output, 200
190
+ return json_output, 200
191
191
 
192
192
 
193
193
  @control.route("/backend_control", methods=['GET'])
@@ -9,7 +9,7 @@
9
9
  {# {% if not deck %}#}
10
10
  {# <a href="{{ url_for('control.disconnect', instrument=instrument) }}" class="stretched-link controller-card" style="float: right;color: red; position: relative;">Disconnect <i class="bi bi-x-square"></i></a>#}
11
11
  <div class="p-4 controller-card">
12
- <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument}}</a></h5>
12
+ <h5 class=""><a href="{{ url_for('control.controllers', instrument=instrument) }}" class="text-dark stretched-link">{{instrument.split(".")[1]}}</a></h5>
13
13
  </div>
14
14
  {# {% else %}#}
15
15
  {# <div class="p-4 controller-card">#}
@@ -19,6 +19,11 @@
19
19
  </div>
20
20
  </div>
21
21
  {% endfor %}
22
+ <div class="d-flex mb-3">
23
+ <a href="{{ url_for('design.download', filetype='proxy') }}" class="btn btn-outline-primary">
24
+ <i class="bi bi-download"></i> Download remote control script
25
+ </a>
26
+ </div>
22
27
  {% if not deck %}
23
28
  <div class="col-xl-3 col-lg-4 col-md-6 mb-4 ">
24
29
  <div class="bg-white rounded shadow-sm position-relative">
@@ -1,7 +1,7 @@
1
- from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app
1
+ from flask import Blueprint, redirect, url_for, flash, request, render_template, session, current_app, jsonify
2
2
  from flask_login import login_required
3
3
 
4
- from ivoryos.utils.db_models import Script, db
4
+ from ivoryos.utils.db_models import Script, db, WorkflowRun, WorkflowStep
5
5
  from ivoryos.utils.utils import get_script_file, post_script_file
6
6
 
7
7
  database = Blueprint('database', __name__, template_folder='templates/database')
@@ -176,13 +176,56 @@ def save_as():
176
176
  """
177
177
  if request.method == "POST":
178
178
  run_name = request.form.get("run_name")
179
+ register_workflow = request.form.get("register_workflow")
179
180
  exist_script = Script.query.get(run_name)
180
181
  if not exist_script:
181
182
  script = get_script_file()
182
183
  script.save_as(run_name)
184
+ script.registered = register_workflow == "on"
183
185
  script.author = session.get('user')
184
186
  post_script_file(script)
185
187
  publish()
186
188
  else:
187
189
  flash("Script name is already exist in database")
188
190
  return redirect(url_for("design.experiment_builder"))
191
+
192
+
193
+ @database.route('/workflow_runs')
194
+ def list_workflows():
195
+ query = WorkflowRun.query
196
+ search_term = request.args.get("keyword", None)
197
+ if search_term:
198
+ query = query.filter(WorkflowRun.name.like(f'%{search_term}%'))
199
+ page = request.args.get('page', default=1, type=int)
200
+ per_page = 10
201
+
202
+ workflows = query.paginate(page=page, per_page=per_page, error_out=False)
203
+ return render_template('workflow_run_database.html', workflows=workflows)
204
+
205
+
206
+ @database.route('/workflow_steps/<int:workflow_id>')
207
+ def get_workflow_steps(workflow_id):
208
+ steps = WorkflowStep.query.filter_by(workflow_id=workflow_id).all()
209
+ steps_data = [step.as_dict() for step in steps]
210
+ return jsonify({'steps': steps_data})
211
+
212
+
213
+ @database.route("/delete_workflow_data/<workflow_id>")
214
+ @login_required
215
+ def delete_workflow_data(workflow_id: str):
216
+ """
217
+ .. :quickref: Database; delete experiment data from database
218
+
219
+ delete workflow data from database
220
+
221
+ .. http:get:: /delete_workflow_data/<workflow_id>
222
+
223
+ :param workflow_id: workflow id
224
+ :type workflow_id: str
225
+ :status 302: redirect to :http:get:`/ivoryos/workflow_runs/`
226
+
227
+ """
228
+ run = WorkflowRun.query.get(workflow_id)
229
+ db.session.delete(run)
230
+ db.session.commit()
231
+ return redirect(url_for('database.list_workflows'))
@@ -26,10 +26,11 @@
26
26
  <tr>
27
27
  <th scope="col">Workflow name</th>
28
28
  <th scope="col">Deck </th>
29
- <th scope="col">Current status</th>
29
+ <th scope="col">Editing</th>
30
30
  <th scope="col">Time created</th>
31
31
  <th scope="col">Last modified</th>
32
32
  <th scope="col">Author</th>
33
+ {# <th scope="col">Registered</th>#}
33
34
  <th scope="col"></th>
34
35
  </tr>
35
36
  </thead>
@@ -42,12 +43,13 @@
42
43
  <td>{{ workflow.time_created }}</td>
43
44
  <td>{{ workflow.last_modified }}</td>
44
45
  <td>{{ workflow.author }}</td>
46
+ {# <td>{{ workflow.registered }}</td>#}
45
47
  <td>
46
48
  {#not workflow.status == "finalized" or#}
47
49
  {% if session['user'] == 'admin' or session['user'] == workflow.author %}
48
50
  <a href="{{ url_for('database.delete_workflow', workflow_name=workflow.name) }}">delete</a>
49
51
  {% else %}
50
- <a class="disabled-link" href="{{ url_for('database.delete_workflow', workflow_name=workflow.name) }}">delete</a>
52
+ <a class="disabled-link">delete</a>
51
53
  {% endif %}
52
54
  <td>
53
55
  </tr>
@@ -0,0 +1,81 @@
1
+ {% extends 'base.html' %}
2
+
3
+ {% block title %}IvoryOS | Design Database{% endblock %}
4
+ {% block body %}
5
+
6
+ <table class="table table-hover" id="workflowResultLibrary">
7
+ <thead>
8
+ <tr>
9
+ <th scope="col">Workflow name</th>
10
+ <th scope="col">Start time</th>
11
+ <th scope="col">End time</th>
12
+ <th scope="col">Data</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ {% for workflow in workflows %}
17
+ <tr>
18
+ <td><a href="{{ url_for('database.get_workflow_steps', workflow_id=workflow.id) }}">{{ workflow.name }}</a></td>
19
+ <td>{{ workflow.start_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.start_time else '' }}</td>
20
+ <td>{{ workflow.end_time.strftime("%Y-%m-%d %H:%M:%S") if workflow.end_time else '' }}</td>
21
+
22
+ <td>
23
+ {% if workflow.data_path %}
24
+ <a href="{{ url_for('design.download_results', filename=workflow.data_path) }}">{{ workflow.data_path }}</a>
25
+ {% endif %}
26
+ </td>
27
+ <td>
28
+ {% if session['user'] == 'admin' or session['user'] == workflow.author %}
29
+ <a href="{{ url_for('database.delete_workflow_data', workflow_id=workflow.id) }}">delete</a>
30
+ {% else %}
31
+ <a class="disabled-link">delete</a>
32
+ {% endif %}
33
+ </td>
34
+ </tr>
35
+ {% endfor %}
36
+ </tbody>
37
+ </table>
38
+
39
+ {# paging#}
40
+ <div class="pagination justify-content-center">
41
+ <div class="page-item {{ 'disabled' if not workflows.has_prev else '' }}">
42
+ <a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.prev_num) }}">Previous</a>
43
+ </div>
44
+ {% for num in workflows.iter_pages() %}
45
+ <div class="page-item">
46
+ <a class="page-link {{ 'active' if num == workflows.page else '' }}" href="{{ url_for('database.list_workflows', page=num) }}">{{ num }}</a>
47
+ </div>
48
+ {% endfor %}
49
+ <div class="page-item {{ 'disabled' if not workflows.has_next else '' }}">
50
+ <a class="page-link" href="{{ url_for('database.list_workflows', page=workflows.next_num) }}">Next</a>
51
+ </div>
52
+ </div>
53
+
54
+ <div id="steps-container"></div>
55
+
56
+ <script>
57
+ function showSteps(workflowId) {
58
+ fetch(`/workflow_steps/${workflowId}`)
59
+ .then(response => response.json())
60
+ .then(data => {
61
+ const container = document.getElementById('steps-container');
62
+ container.innerHTML = ''; // Clear previous content
63
+ const stepsList = document.createElement('ul');
64
+
65
+ data.steps.forEach(step => {
66
+ const li = document.createElement('li');
67
+ li.innerHTML = `
68
+ <strong>Step: </strong> ${step.method_name} <br>
69
+ <strong>Start Time:</strong> ${step.start_time} <br>
70
+ <strong>End Time:</strong> ${step.end_time} <br>
71
+ <strong>Human Intervention:</strong> ${step.run_error ? 'Yes' : 'No'}
72
+ `;
73
+ stepsList.appendChild(li);
74
+ });
75
+
76
+ container.appendChild(stepsList);
77
+ });
78
+ }
79
+ </script>
80
+
81
+ {% endblock %}
@@ -11,11 +11,13 @@ from flask_socketio import SocketIO
11
11
  from werkzeug.utils import secure_filename
12
12
 
13
13
  from ivoryos.utils import utils
14
+ from ivoryos.utils.client_proxy import create_function, export_to_python
14
15
  from ivoryos.utils.global_config import GlobalConfig
15
16
  from ivoryos.utils.form import create_builtin_form, create_action_button, format_name, create_form_from_pseudo, \
16
- create_form_from_action
17
+ create_form_from_action, create_all_builtin_forms
17
18
  from ivoryos.utils.db_models import Script
18
19
  from ivoryos.utils.script_runner import ScriptRunner
20
+ # from ivoryos.utils.utils import load_workflows
19
21
 
20
22
  socketio = SocketIO()
21
23
  design = Blueprint('design', __name__, template_folder='templates/design')
@@ -92,6 +94,9 @@ def experiment_builder(instrument=None):
92
94
  """
93
95
  deck = global_config.deck
94
96
  script = utils.get_script_file()
97
+ # load_workflows(script)
98
+ # registered_workflows = global_config.registered_workflows
99
+
95
100
  if deck and script.deck is None:
96
101
  script.deck = os.path.splitext(os.path.basename(deck.__file__))[
97
102
  0] if deck.__name__ == "__main__" else deck.__name__
@@ -114,7 +119,10 @@ def experiment_builder(instrument=None):
114
119
 
115
120
  functions = {}
116
121
  if deck:
117
- deck_variables = global_config.deck_snapshot.keys()
122
+ deck_variables = list(global_config.deck_snapshot.keys())
123
+ # deck_variables.insert(0, "registered_workflows")
124
+ deck_variables.insert(0, "flow_control")
125
+
118
126
  else:
119
127
  deck_variables = list(pseudo_deck.keys()) if pseudo_deck else []
120
128
  deck_variables.remove("deck_name") if len(deck_variables) > 0 else deck_variables
@@ -122,23 +130,31 @@ def experiment_builder(instrument=None):
122
130
  if edit_action_info:
123
131
  forms = create_form_from_action(edit_action_info, script=script)
124
132
  elif instrument:
125
- if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
126
- forms = create_builtin_form(instrument, script=script)
133
+ # if instrument in ['if', 'while', 'variable', 'wait', 'repeat']:
134
+ # forms = create_builtin_form(instrument, script=script)
135
+ if instrument == 'flow_control':
136
+ forms = create_all_builtin_forms(script=script)
137
+ # elif instrument == 'registered_workflows':
138
+ # functions = utils._inspect_class(registered_workflows)
139
+ # # forms = create_workflow_forms(script=script)
140
+ # forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
141
+ elif instrument in global_config.defined_variables.keys():
142
+ _object = global_config.defined_variables.get(instrument)
143
+ functions = utils._inspect_class(_object)
144
+ forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
127
145
  else:
128
146
  if deck:
129
147
  functions = global_config.deck_snapshot.get(instrument, {})
130
148
  elif pseudo_deck:
131
149
  functions = pseudo_deck.get(instrument, {})
132
- # print(function_metadata)
133
- # functions = {key: data.get('signature', {}) for key, data in function_metadata.items()}
134
150
  forms = create_form_from_pseudo(pseudo=functions, autofill=autofill, script=script)
135
151
  if request.method == 'POST' and "hidden_name" in request.form:
136
152
  # all_kwargs = request.form.copy()
137
153
  method_name = request.form.get("hidden_name", None)
138
154
  # if method_name is not None:
139
155
  form = forms.get(method_name)
156
+ insert_position = request.form.get("drop_target_id", None)
140
157
  kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
141
-
142
158
  if form and form.validate_on_submit():
143
159
  function_name = kwargs.pop("hidden_name")
144
160
  save_data = kwargs.pop('return', '')
@@ -151,30 +167,57 @@ def experiment_builder(instrument=None):
151
167
  "args": kwargs,
152
168
  "return": save_data,
153
169
  'arg_types': primitive_arg_types}
154
- script.add_action(action=action)
170
+ script.add_action(action=action, insert_position=insert_position)
155
171
  else:
156
172
  flash(form.errors)
157
173
 
158
174
  elif request.method == 'POST' and "builtin_name" in request.form:
159
- kwargs = {field.name: field.data for field in forms if field.name != 'csrf_token'}
160
- if forms.validate_on_submit():
175
+ function_name = request.form.get("builtin_name")
176
+ form = forms.get(function_name)
177
+ kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
178
+ insert_position = request.form.get("drop_target_id", None)
179
+
180
+ if form.validate_on_submit():
161
181
  # print(kwargs)
162
182
  logic_type = kwargs.pop('builtin_name')
163
183
  if 'variable' in kwargs:
164
184
  try:
165
- script.add_variable(**kwargs)
185
+ script.add_variable(**kwargs, insert_position=insert_position)
166
186
  except ValueError:
167
187
  flash("Invalid variable type")
168
188
  else:
169
- script.add_logic_action(logic_type=logic_type, **kwargs)
189
+ script.add_logic_action(logic_type=logic_type, **kwargs, insert_position=insert_position)
170
190
  else:
171
- flash(forms.errors)
191
+ flash(form.errors)
192
+ elif request.method == 'POST' and "workflow_name" in request.form:
193
+ workflow_name = request.form.get("workflow_name")
194
+ form = forms.get(workflow_name)
195
+ kwargs = {field.name: field.data for field in form if field.name != 'csrf_token'}
196
+ insert_position = request.form.get("drop_target_id", None)
197
+
198
+ if form.validate_on_submit():
199
+ # workflow_name = kwargs.pop('workflow_name')
200
+ save_data = kwargs.pop('return', '')
201
+
202
+ primitive_arg_types = utils.get_arg_type(kwargs, functions[workflow_name])
172
203
 
173
- # toggle autofill
204
+ script.eval_list(kwargs, primitive_arg_types)
205
+ kwargs = script.validate_variables(kwargs)
206
+ action = {"instrument": instrument, "action": workflow_name,
207
+ "args": kwargs,
208
+ "return": save_data,
209
+ 'arg_types': primitive_arg_types}
210
+ script.add_action(action=action, insert_position=insert_position)
211
+ script.add_workflow(**kwargs, insert_position=insert_position)
212
+ else:
213
+ flash(form.errors)
214
+
215
+ # toggle autofill, autofill doesn't apply to control flow ops
174
216
  elif request.method == 'POST' and "autofill" in request.form:
175
217
  autofill = not autofill
176
- forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
177
218
  session['autofill'] = autofill
219
+ if not instrument == 'flow_control':
220
+ forms = create_form_from_pseudo(functions, autofill=autofill, script=script)
178
221
 
179
222
  utils.post_script_file(script)
180
223
  design_buttons = create_action_button(script)
@@ -318,7 +361,7 @@ def experiment_run():
318
361
  run_name = script.validate_function_name(run_name)
319
362
  runner.run_script(script=script, run_name=run_name, config=config, bo_args=bo_args,
320
363
  logger=g.logger, socketio=g.socketio, repeat_count=repeat,
321
- output_path=datapath
364
+ output_path=datapath, current_app=current_app._get_current_object()
322
365
  )
323
366
  if utils.check_config_duplicate(config):
324
367
  flash(f"WARNING: Duplicate in config entries.")
@@ -329,7 +372,7 @@ def experiment_run():
329
372
  return_list=return_list, config_list=config_list, config_file_list=config_file_list,
330
373
  config_preview=config_preview, data_list=data_list, config_type_list=config_type_list,
331
374
  no_deck_warning=no_deck_warning, dismiss=dismiss, design_buttons=design_buttons,
332
- history=deck_list)
375
+ history=deck_list, pause_status=runner.pause_status())
333
376
 
334
377
 
335
378
  @design.route("/toggle_script_type/<stype>")
@@ -483,7 +526,20 @@ def download(filetype):
483
526
  outfile.write(json_object)
484
527
  elif filetype == "python":
485
528
  filepath = os.path.join(current_app.config["SCRIPT_FOLDER"], f"{run_name}.py")
486
-
529
+ elif filetype == "proxy":
530
+ snapshot = global_config.deck_snapshot.copy()
531
+ class_definitions = {}
532
+ # Iterate through each instrument in the snapshot
533
+ for instrument_key, instrument_data in snapshot.items():
534
+ # Iterate through each function associated with the current instrument
535
+ for function_key, function_data in instrument_data.items():
536
+ # Convert the function signature to a string representation
537
+ function_data['signature'] = str(function_data['signature'])
538
+ class_name = instrument_key.split('.')[-1] # Extracting the class name from the path
539
+ class_definitions[class_name.capitalize()] = create_function(request.url_root, class_name, instrument_data)
540
+ # Export the generated class definitions to a .py script
541
+ export_to_python(class_definitions, current_app.config["OUTPUT_FOLDER"])
542
+ filepath = os.path.join(current_app.config["OUTPUT_FOLDER"], "generated_proxy.py")
487
543
  return send_file(os.path.abspath(filepath), as_attachment=True)
488
544
 
489
545